@symbiote-native/solid 0.0.1 → 1.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 (112) hide show
  1. package/README.md +25 -14
  2. package/babel-lower-host-primitives.cjs +323 -0
  3. package/babel-preset.cjs +5 -0
  4. package/build/bootstrap.d.ts +7 -0
  5. package/build/bootstrap.js +18 -0
  6. package/build/components/activity-indicator/index.android.d.ts +2 -0
  7. package/build/components/activity-indicator/index.android.js +11 -0
  8. package/build/components/activity-indicator/index.d.ts +1 -0
  9. package/build/components/activity-indicator/index.ios.d.ts +2 -0
  10. package/build/components/activity-indicator/index.ios.js +10 -0
  11. package/build/components/activity-indicator/index.js +4 -0
  12. package/build/components/activity-indicator/shared.d.ts +8 -0
  13. package/build/components/activity-indicator/shared.js +51 -0
  14. package/build/components/button.d.ts +7 -0
  15. package/build/components/button.jsx +45 -0
  16. package/build/components/flat-list/index.d.ts +10 -0
  17. package/build/components/flat-list/index.jsx +118 -0
  18. package/build/components/image-background.d.ts +10 -0
  19. package/build/components/image-background.jsx +74 -0
  20. package/build/components/image.d.ts +9 -0
  21. package/build/components/image.js +75 -0
  22. package/build/components/input-accessory-view.d.ts +11 -0
  23. package/build/components/input-accessory-view.jsx +39 -0
  24. package/build/components/keyboard-avoiding-view.d.ts +15 -0
  25. package/build/components/keyboard-avoiding-view.jsx +152 -0
  26. package/build/components/modal.d.ts +24 -0
  27. package/build/components/modal.jsx +125 -0
  28. package/build/components/pressable.d.ts +31 -0
  29. package/build/components/pressable.jsx +211 -0
  30. package/build/components/refresh-control.d.ts +18 -0
  31. package/build/components/refresh-control.jsx +36 -0
  32. package/build/components/safe-area-view.d.ts +11 -0
  33. package/build/components/safe-area-view.jsx +24 -0
  34. package/build/components/scroll-view/index.android.d.ts +2 -0
  35. package/build/components/scroll-view/index.android.js +11 -0
  36. package/build/components/scroll-view/index.d.ts +2 -0
  37. package/build/components/scroll-view/index.ios.d.ts +2 -0
  38. package/build/components/scroll-view/index.ios.js +8 -0
  39. package/build/components/scroll-view/index.js +9 -0
  40. package/build/components/scroll-view/shared.d.ts +85 -0
  41. package/build/components/scroll-view/shared.jsx +334 -0
  42. package/build/components/scroll-view/sticky-header.d.ts +18 -0
  43. package/build/components/scroll-view/sticky-header.jsx +182 -0
  44. package/build/components/section-list/index.d.ts +6 -0
  45. package/build/components/section-list/index.jsx +8 -0
  46. package/build/components/switch/index.android.d.ts +2 -0
  47. package/build/components/switch/index.android.js +12 -0
  48. package/build/components/switch/index.d.ts +1 -0
  49. package/build/components/switch/index.ios.d.ts +2 -0
  50. package/build/components/switch/index.ios.js +11 -0
  51. package/build/components/switch/index.js +4 -0
  52. package/build/components/switch/shared.d.ts +11 -0
  53. package/build/components/switch/shared.js +87 -0
  54. package/build/components/text-input.d.ts +10 -0
  55. package/build/components/text-input.js +290 -0
  56. package/build/components/text.d.ts +26 -0
  57. package/build/components/text.jsx +27 -0
  58. package/build/components/touchable/index.d.ts +22 -0
  59. package/build/components/touchable/index.jsx +247 -0
  60. package/build/components/touchable-native-feedback/index.d.ts +16 -0
  61. package/build/components/touchable-native-feedback/index.jsx +68 -0
  62. package/build/components/view.d.ts +32 -0
  63. package/build/components/view.jsx +49 -0
  64. package/build/components/virtualized-list/index.android.d.ts +2 -0
  65. package/build/components/virtualized-list/index.android.js +9 -0
  66. package/build/components/virtualized-list/index.d.ts +1 -0
  67. package/build/components/virtualized-list/index.ios.d.ts +2 -0
  68. package/build/components/virtualized-list/index.ios.js +9 -0
  69. package/build/components/virtualized-list/index.js +4 -0
  70. package/build/components/virtualized-list/shared.d.ts +75 -0
  71. package/build/components/virtualized-list/shared.jsx +782 -0
  72. package/build/components/virtualized-section-list/index.d.ts +62 -0
  73. package/build/components/virtualized-section-list/index.jsx +145 -0
  74. package/build/components.d.ts +43 -0
  75. package/build/components.js +54 -0
  76. package/build/create-portal/index.d.ts +11 -0
  77. package/build/create-portal/index.jsx +88 -0
  78. package/build/create-tunnel/index.d.ts +13 -0
  79. package/build/create-tunnel/index.jsx +53 -0
  80. package/build/descriptor-to-solid.d.ts +3 -0
  81. package/build/descriptor-to-solid.js +91 -0
  82. package/build/host-instance.d.ts +2 -0
  83. package/build/host-instance.js +36 -0
  84. package/build/index.d.ts +31 -0
  85. package/build/index.js +98 -0
  86. package/build/jsx-runtime.d.ts +26 -0
  87. package/build/jsx-runtime.js +46 -0
  88. package/build/modules/animated/create-animated-component.d.ts +10 -0
  89. package/build/modules/animated/create-animated-component.jsx +127 -0
  90. package/build/modules/animated/index.d.ts +54 -0
  91. package/build/modules/animated/index.js +78 -0
  92. package/build/modules/app-registry/index.d.ts +7 -0
  93. package/build/modules/app-registry/index.js +37 -0
  94. package/build/modules/status-bar.d.ts +7 -0
  95. package/build/modules/status-bar.js +45 -0
  96. package/build/primitives/create-color-scheme.d.ts +3 -0
  97. package/build/primitives/create-color-scheme.js +33 -0
  98. package/build/primitives/create-window-dimensions.d.ts +3 -0
  99. package/build/primitives/create-window-dimensions.js +34 -0
  100. package/build/register.d.ts +1 -0
  101. package/build/register.js +31 -0
  102. package/build/render.d.ts +7 -0
  103. package/build/render.js +91 -0
  104. package/build/renderer.d.ts +8 -0
  105. package/build/renderer.js +259 -0
  106. package/build/state-style.d.ts +1 -0
  107. package/build/state-style.js +13 -0
  108. package/build/utils/host-ref.d.ts +3 -0
  109. package/build/utils/host-ref.js +22 -0
  110. package/build/utils/stable-keys.d.ts +1 -0
  111. package/build/utils/stable-keys.js +31 -0
  112. package/package.json +24 -8
@@ -0,0 +1,118 @@
1
+ // FlatList — the convenience surface over VirtualizedList. It takes a plain `data` array and
2
+ // derives the getItem/getItemCount access protocol itself, so a caller never writes them, and
3
+ // `numColumns` packs that many items into each row so the virtualized stream is ROWS rather than
4
+ // items (RN's FlatList). Everything else — windowing, spacers, viewability, batching, sticky
5
+ // headers, pull-to-refresh, the imperative scrolls — is inherited from VirtualizedList; the data
6
+ // shaping and the row/viewability/separator transforms come from @symbiote-native/components and
7
+ // are shared verbatim with React, Vue, Svelte and Angular. This file adds Solid's lifecycle and
8
+ // nothing else.
9
+ //
10
+ // TWO SOLID FACTS SHAPE IT (.claude/rules/solid-descriptor-bridge.md):
11
+ //
12
+ // 1. `renderItem` takes an ACCESSOR, matching this adapter's VirtualizedList and diverging from
13
+ // React's snapshot signature. Solid has no reconciler under the render prop, so a value would
14
+ // freeze the row at its mount-time item.
15
+ // 2. The columns inside a packed row ride an <Index>, not a <For>. That is the exact MIRROR of
16
+ // VirtualizedList's choice and for the same reason read the other way round: a cell key moves
17
+ // between rows as the window slides, so the CELLS are keyed by value; a column is a fixed
18
+ // positional slot inside its row — column 1 of row 3 is always column 1 of row 3 — so keying it
19
+ // by value would destroy and rebuild the whole column subtree every time the data array is
20
+ // replaced with fresh item objects. <Index> keys by position and hands the item down as an
21
+ // accessor, so only the leaf that reads it re-runs.
22
+ //
23
+ // The imperative handle needs no delegation layer here, unlike Vue's and Svelte's. `ref` is an
24
+ // ordinary prop in the passthrough tail, so it lands on the inner VirtualizedList directly and the
25
+ // caller receives that list's own handle — FlatList's handle IS VirtualizedList's (RN says the
26
+ // same).
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';
30
+ import { VirtualizedList, } from '../virtualized-list';
31
+ // What FlatList itself consumes or re-shapes. Everything NOT listed here is passthrough and rides
32
+ // down onto the inner list untouched — `ref` among it, which is why no delegate handle is needed.
33
+ const OWN_PROPS = [
34
+ 'data',
35
+ 'numColumns',
36
+ 'columnWrapperStyle',
37
+ 'renderItem',
38
+ 'keyExtractor',
39
+ 'ItemSeparatorComponent',
40
+ 'onViewableItemsChanged',
41
+ 'viewabilityConfigCallbackPairs',
42
+ ];
43
+ const ROW_STYLE = { flexDirection: 'row' };
44
+ // Equal weight per column, so `numColumns` columns split the row evenly (RN's own multi-column
45
+ // FlatList). A cell that sized itself would leave ragged columns.
46
+ const COLUMN_STYLE = { flex: 1 };
47
+ export function FlatList(props) {
48
+ const [, passthrough] = splitProps(props, OWN_PROPS);
49
+ const columnCount = () => props.numColumns ?? SINGLE_COLUMN;
50
+ // RN treats anything at or below one column as an ordinary single-column list rather than an
51
+ // error, and so does adapters/react.
52
+ const isMultiColumn = () => columnCount() > SINGLE_COLUMN;
53
+ const rows = createMemo(() => {
54
+ const columns = columnCount();
55
+ // Logged from inside the memo, not from the body: a Solid body runs ONCE, so a line emitted
56
+ // there would report the mount-time shape forever and be actively misleading on a data change.
57
+ dlog(`Solid FlatList over ${props.data.length} items, ${columns} column(s)`);
58
+ return columns > SINGLE_COLUMN ? chunkIntoRows(props.data, columns) : [];
59
+ });
60
+ // A bare string is a registered class name and has to be resolved here — the row view's style
61
+ // prop takes objects/arrays, so an unresolved string would silently paint nothing.
62
+ const rowStyle = createMemo(() => [
63
+ ROW_STYLE,
64
+ typeof props.columnWrapperStyle === 'string'
65
+ ? resolveClassName(props.columnWrapperStyle)
66
+ : props.columnWrapperStyle,
67
+ ]);
68
+ function renderRow(info) {
69
+ return (<symbiote-view style={rowStyle()}>
70
+ <Index each={info().item.items}>
71
+ {(item, column) => {
72
+ // Called ONCE and untracked, the shape VirtualizedList's own cell build uses: a
73
+ // tracked call would put every signal renderItem reads into this column's insert
74
+ // effect and rebuild the subtree instead of updating its leaf.
75
+ const content = untrack(() => props.renderItem(() => ({
76
+ item: item(),
77
+ index: info().item.startIndex + column,
78
+ // The row IS the virtualized cell, so every column in it shares the row's
79
+ // separators handle — the divider sits between rows, not between columns.
80
+ separators: info().separators,
81
+ })));
82
+ return (<symbiote-view style={COLUMN_STYLE}>{content}</symbiote-view>);
83
+ }}
84
+ </Index>
85
+ </symbiote-view>);
86
+ }
87
+ // The divider between rows shows real items (last of the row above, first of the row below), so
88
+ // the caller's separator, typed on ItemT, never sees the IRow wrapper.
89
+ const rowSeparatorComponent = createMemo(() => {
90
+ const Separator = props.ItemSeparatorComponent;
91
+ if (Separator === undefined)
92
+ return undefined;
93
+ return (rowProps) => (<Separator {...rowProps} leadingItem={lastItemOfRow(rowProps.leadingItem)} trailingItem={firstItemOfRow(rowProps.trailingItem)}/>);
94
+ });
95
+ // Viewability over rows expands back to per-item tokens, so the caller sees item-level
96
+ // visibility rather than row-level (shared expandRowViewability).
97
+ const rowViewability = createMemo(() => {
98
+ const onChanged = props.onViewableItemsChanged;
99
+ if (onChanged === undefined)
100
+ return undefined;
101
+ return (rowInfo) => onChanged(expandRowViewability(rowInfo, props.keyExtractor));
102
+ });
103
+ const rowViewabilityPairs = createMemo(() => props.viewabilityConfigCallbackPairs?.map(pair => ({
104
+ viewabilityConfig: pair.viewabilityConfig,
105
+ onViewableItemsChanged: (rowInfo) => {
106
+ pair.onViewableItemsChanged(expandRowViewability(rowInfo, props.keyExtractor));
107
+ },
108
+ })));
109
+ // The two paths instantiate VirtualizedList over DIFFERENT item types — ItemT vs IRow<ItemT> —
110
+ // so they are two elements, not one with switching props: a single instantiation would have to
111
+ // union the two and narrow ItemT | IRow<ItemT> back apart at runtime, which nothing can do
112
+ // soundly for an arbitrary ItemT. <Show> memoizes on the flip alone, so a numColumns change
113
+ // rebuilds (RN documents changing it live as unsupported — "change the key prop" — which is the
114
+ // 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={rowKeyExtractor} ItemSeparatorComponent={rowSeparatorComponent()} onViewableItemsChanged={rowViewability()} viewabilityConfigCallbackPairs={rowViewabilityPairs()}/>
117
+ </Show>);
118
+ }
@@ -0,0 +1,10 @@
1
+ import type { JSX } from '../jsx-runtime';
2
+ import { type IImageProps } from '@symbiote-native/components';
3
+ import { type IClassNameValue, type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
4
+ export interface IImageBackgroundProps extends Omit<IImageProps, 'style'> {
5
+ style?: IStyleProp<IViewStyle>;
6
+ imageStyle?: IStyleProp<IViewStyle> | string;
7
+ class?: IClassNameValue;
8
+ children?: JSX.Element;
9
+ }
10
+ export declare function ImageBackground(props: IImageBackgroundProps): JSX.Element;
@@ -0,0 +1,74 @@
1
+ // ImageBackground — the Solid lifecycle half. The composition (the absolute-fill Image behind the
2
+ // children, the wrapper-dimension proxy onto that Image, the imageStyle-last merge) lives
3
+ // framework-agnostic in @symbiote-native/components' renderImageBackground and is shared verbatim
4
+ // with React, Vue and Svelte. Solid supplies only the reactivity, the class-name resolution and
5
+ // the child order.
6
+ //
7
+ // SPLIT DOWN THE MIDDLE, and the seam is the children rule. The wrapper hosts a live Solid subtree
8
+ // (the overlay content), which only the reconciler can reduce, so it stays a literal
9
+ // `symbiote-view` carrying the shared fn's own props — the same shape View and SafeAreaView use,
10
+ // and the same one Svelte's ImageBackground lands on. The inner image is a leaf built entirely
11
+ // from VALUES, so it goes through descriptorToSolid, which builds it once and re-props it in place
12
+ // (replacing it would restart the download and drop the decoded bitmap).
13
+ //
14
+ // FLAT FILE, no folder: there are no platform or shared variants to group
15
+ // (`symbiote-file-layout` — only genuine groups get a folder).
16
+ //
17
+ // NOTHING here destructures `props` at setup. Solid props are getters and a component body runs
18
+ // ONCE; splitProps keeps every bucket reactive, and each read happens inside an accessor.
19
+ import { createMemo, splitProps } from 'solid-js';
20
+ import { createDescriptorShapeGuard, renderImageBackground, resolveAccessibilityProps, } from '@symbiote-native/components';
21
+ import { resolveClassName, } from '@symbiote-native/engine';
22
+ import { descriptorToSolid } from '../descriptor-to-solid.js';
23
+ // renderImage's typed view fields — the props it CONSUMES rather than forwards. Split off so they
24
+ // never also ride through `passthrough`: the W3C aliases in particular must not reach Fabric raw,
25
+ // since native sees only the resolved `source` array they fold into. `style` is absent on purpose
26
+ // — the inner image's style is computed by renderImageBackground, never taken from the caller.
27
+ const IMAGE_VIEW_PROPS = [
28
+ 'source',
29
+ 'defaultSource',
30
+ 'loadingIndicatorSource',
31
+ 'resizeMode',
32
+ 'tintColor',
33
+ 'src',
34
+ 'srcSet',
35
+ 'alt',
36
+ 'width',
37
+ 'height',
38
+ 'crossOrigin',
39
+ 'referrerPolicy',
40
+ ];
41
+ const shape = createDescriptorShapeGuard('ImageBackground');
42
+ export function ImageBackground(props) {
43
+ const [local, view, rest] = splitProps(props, ['children', 'style', 'imageStyle', 'class'], IMAGE_VIEW_PROPS);
44
+ // One memo, so the wrapper bag and the inner image derive from a SINGLE call per change.
45
+ const wrapper = createMemo(() => renderImageBackground({
46
+ style: local.style,
47
+ // A bare string is a registered class name and has to become a style object here, before
48
+ // it reaches the shared fn — which merges it, and knows nothing about the registry.
49
+ imageStyle: typeof local.imageStyle === 'string'
50
+ ? resolveClassName(local.imageStyle)
51
+ : local.imageStyle,
52
+ image: {
53
+ ...view,
54
+ // The wrapper is a raw host, not the View component, so the aria/role fold is this
55
+ // component's own job; the resolved surface rides the IMAGE, matching every other adapter.
56
+ passthrough: { ...resolveAccessibilityProps(rest) },
57
+ },
58
+ }));
59
+ const backgroundImage = descriptorToSolid(() => shape.asElement(wrapper().children[0]));
60
+ // ONE object literal rather than a spread-then-override on the tag: `class` is legitimately
61
+ // undefined, and Solid's mergeProps (which the compiler uses for a spread followed by an
62
+ // explicit prop) would discard that undefined instead of applying it
63
+ // (.claude/rules/solid-descriptor-bridge.md §6). No withStableKeys — renderImageBackground's
64
+ // wrapper props are `{ style }` unconditionally, so no key can vanish between runs.
65
+ const wrapperBag = () => ({
66
+ ...wrapper().props,
67
+ class: local.class,
68
+ });
69
+ // The user children come AFTER the image in child order, which is what makes them paint on top.
70
+ return (<symbiote-view {...wrapperBag()}>
71
+ {backgroundImage}
72
+ {local.children}
73
+ </symbiote-view>);
74
+ }
@@ -0,0 +1,9 @@
1
+ import type { JSX } from '../jsx-runtime';
2
+ import { type IImageProps as IImageBaseProps, type IImageStatics } from '@symbiote-native/components';
3
+ import type { IClassNameValue } from '@symbiote-native/engine';
4
+ export type { IImageStatics } from '@symbiote-native/components';
5
+ export type IImageProps = IImageBaseProps & {
6
+ class?: IClassNameValue;
7
+ };
8
+ export type IImageWithStatics = ((props: IImageProps) => JSX.Element) & IImageStatics;
9
+ export declare const Image: IImageWithStatics;
@@ -0,0 +1,75 @@
1
+ // Image — the Solid lifecycle half. Everything that turns props into a native prop bag (the
2
+ // source / src / srcSet resolution, the width+height fold into style, resizeMode and tintColor
3
+ // read back off the style, alt -> accessibilityLabel, the array shape native expects) lives
4
+ // framework-agnostic in @symbiote-native/components' renderImage, shared verbatim with React, Vue
5
+ // and Svelte. Solid supplies only the reactivity and the Descriptor bridge.
6
+ //
7
+ // PLAIN .ts, NOT .tsx, unlike ./view.tsx and ./text.tsx — and the difference is the children rule,
8
+ // not a style choice. View and Text take a live subtree of the user's components, which only the
9
+ // framework's own reconciler can reduce, so they emit real JSX. An image is a leaf built entirely
10
+ // from VALUES, so it goes through renderImage + descriptorToSolid exactly like Switch does
11
+ // (.claude/rules/component-render-fn-boundary.md).
12
+ //
13
+ // NOTHING here destructures `props`. Solid props are getters and a component body runs ONCE;
14
+ // splitProps is the idiomatic split that keeps both halves reactive, and every read below happens
15
+ // inside the descriptor accessor descriptorToSolid re-runs.
16
+ //
17
+ // ASSET RESOLUTION IS LAZY BY CONSTRUCTION, and that is what lets an app drop the hand-rolled
18
+ // resolveImageSource() call it needed while this component did not exist. renderImage calls
19
+ // resolveImageSource at RENDER time, and the resolver behind it is installed by bootstrapHost,
20
+ // which createApp().mount() runs before AppRegistry ever renders a root. So a `require('./x.png')`
21
+ // asset id handed to `source` is resolved by RN's own resolveAssetSource on the way to Fabric,
22
+ // with no ordering hazard: a module-scope resolve would run one step too early, an accessor-time
23
+ // one cannot.
24
+ import { splitProps } from 'solid-js';
25
+ import { imageStatics, renderImage, resolveAccessibilityProps, } from '@symbiote-native/components';
26
+ import { descriptorToSolid } from '../descriptor-to-solid.js';
27
+ // renderImage's typed view fields — the props it CONSUMES rather than forwards. Split off here so
28
+ // they never also ride through `passthrough`: the W3C aliases in particular must not reach Fabric
29
+ // raw, since native sees only the resolved `source` array they fold into. Everything else (the
30
+ // load events, blurRadius, capInsets, resizeMethod, fadeDuration, testID, `class`, a caller's own
31
+ // `ref`, the accessibility props) stays in `rest` and lands on the host image untouched.
32
+ const VIEW_PROPS = [
33
+ 'source',
34
+ 'defaultSource',
35
+ 'loadingIndicatorSource',
36
+ 'style',
37
+ 'resizeMode',
38
+ 'tintColor',
39
+ 'src',
40
+ 'srcSet',
41
+ 'alt',
42
+ 'width',
43
+ 'height',
44
+ 'crossOrigin',
45
+ 'referrerPolicy',
46
+ ];
47
+ function ImageComponent(props) {
48
+ const [view, rest] = splitProps(props, VIEW_PROPS);
49
+ // The node is created once and kept by identity — descriptorToSolid wires every prop through a
50
+ // render effect on that same node, so a later source/style/alt change re-commits the existing
51
+ // image rather than replacing it (which would restart the download and drop the decoded bitmap).
52
+ return descriptorToSolid(() => renderImage({
53
+ source: view.source,
54
+ defaultSource: view.defaultSource,
55
+ loadingIndicatorSource: view.loadingIndicatorSource,
56
+ style: view.style,
57
+ resizeMode: view.resizeMode,
58
+ tintColor: view.tintColor,
59
+ src: view.src,
60
+ srcSet: view.srcSet,
61
+ alt: view.alt,
62
+ width: view.width,
63
+ height: view.height,
64
+ crossOrigin: view.crossOrigin,
65
+ referrerPolicy: view.referrerPolicy,
66
+ // Image owns its host element rather than rendering through a symbiote View, so folding
67
+ // aria/role into the canonical accessibility* props is its own job, like Switch's.
68
+ // renderImage's own `alt` fold then defers to an explicit accessibilityLabel found here.
69
+ passthrough: { ...resolveAccessibilityProps(rest) },
70
+ }));
71
+ }
72
+ // Statics attached like RN's own `Image.getSize` / `prefetch` / `queryCache` / `abortPrefetch` /
73
+ // `getSizeWithHeaders` / `resolveAssetSource`, by identity off the same @symbiote-native/engine
74
+ // module React, Vue and Svelte attach — never a Solid-side reimplementation.
75
+ export const Image = Object.assign(ImageComponent, imageStatics);
@@ -0,0 +1,11 @@
1
+ import type { JSX } from '../jsx-runtime';
2
+ import { type IAccessibilityProps, type IAriaProps } from '@symbiote-native/components';
3
+ import type { IClassNameValue, IStyleProp, IViewStyle } from '@symbiote-native/engine';
4
+ export interface IInputAccessoryViewProps extends IAccessibilityProps, IAriaProps {
5
+ nativeID?: string;
6
+ backgroundColor?: string;
7
+ style?: IStyleProp<IViewStyle>;
8
+ class?: IClassNameValue;
9
+ children?: JSX.Element;
10
+ }
11
+ export declare function InputAccessoryView(props: IInputAccessoryViewProps): JSX.Element;
@@ -0,0 +1,39 @@
1
+ // InputAccessoryView — the Solid lifecycle half (iOS). The host-node assembly (nativeID /
2
+ // backgroundColor / style / passthrough forwarding onto `symbiote-input-accessory-view`) lives
3
+ // framework-agnostic in @symbiote-native/components' renderInputAccessoryView and is shared
4
+ // verbatim with React, Vue and Svelte; Solid supplies only the reactivity and the children.
5
+ //
6
+ // REAL JSX, NOT descriptorToSolid — the same rule View and SafeAreaView follow. The render fn
7
+ // returns a host with ZERO structural children by contract; the toolbar content is a live Solid
8
+ // subtree only the reconciler can reduce, so the tag stays literal and the shared fn contributes
9
+ // its PROPS. The literal tag is safe because renderInputAccessoryView's `type` is a constant.
10
+ //
11
+ // NOTHING here destructures `props` at setup. Solid props are getters and a component body runs
12
+ // ONCE; splitProps keeps both halves reactive, and every read happens inside the bag accessor
13
+ // that `spread` re-runs as a render effect on the SAME host node.
14
+ import { splitProps } from 'solid-js';
15
+ import { renderInputAccessoryView, resolveAccessibilityProps, } from '@symbiote-native/components';
16
+ import { withStableKeys } from '../utils/stable-keys.js';
17
+ export function InputAccessoryView(props) {
18
+ const [local, rest] = splitProps(props, [
19
+ 'children',
20
+ 'nativeID',
21
+ 'backgroundColor',
22
+ 'style',
23
+ ]);
24
+ // withStableKeys is load-bearing twice over: renderInputAccessoryView emits `nativeID` and
25
+ // `backgroundColor` CONDITIONALLY, and resolveAccessibilityProps has two branches with
26
+ // different key sets. Solid's `spread` has no removal pass, so a key that vanishes between
27
+ // runs would keep its last value on the native view forever (see utils/stable-keys.ts).
28
+ const bag = withStableKeys(() => renderInputAccessoryView({
29
+ nativeID: local.nativeID,
30
+ backgroundColor: local.backgroundColor,
31
+ style: local.style,
32
+ // This component owns its host element rather than rendering through a View, so folding
33
+ // aria/role into the canonical accessibility* props is its own job, like Image's.
34
+ passthrough: { ...resolveAccessibilityProps(rest) },
35
+ }).props);
36
+ return (<symbiote-input-accessory-view {...bag()}>
37
+ {local.children}
38
+ </symbiote-input-accessory-view>);
39
+ }
@@ -0,0 +1,15 @@
1
+ import type { JSX } from '../jsx-runtime';
2
+ import { type IAccessibilityProps, type IAriaProps, type IKeyboardAvoidingBehavior } from '@symbiote-native/components';
3
+ import { type IClassNameValue, type IStyleProp, type ISymbioteEvent, type IViewStyle } from '@symbiote-native/engine';
4
+ export type { IKeyboardAvoidingBehavior } from '@symbiote-native/components';
5
+ export interface IKeyboardAvoidingViewProps extends IAccessibilityProps, IAriaProps {
6
+ behavior?: IKeyboardAvoidingBehavior;
7
+ enabled?: boolean;
8
+ keyboardVerticalOffset?: number;
9
+ contentContainerStyle?: IStyleProp<IViewStyle>;
10
+ style?: IStyleProp<IViewStyle>;
11
+ class?: IClassNameValue;
12
+ onLayout?: (event: ISymbioteEvent) => void;
13
+ children?: JSX.Element;
14
+ }
15
+ export declare function KeyboardAvoidingView(props: IKeyboardAvoidingViewProps): JSX.Element;
@@ -0,0 +1,152 @@
1
+ // KeyboardAvoidingView — the Solid lifecycle half. It composes a wrapper View and shifts it out of
2
+ // the keyboard's way as the keyboard shows/hides. The inset math (readKeyboardFrame /
3
+ // readLayoutFrame / computeInset) and the behavior → style/structure decision
4
+ // (resolveKeyboardAvoidingLayout) are framework-agnostic and live in @symbiote-native/components,
5
+ // shared verbatim with React, Vue, Svelte and Angular. Solid supplies only the reactivity: one
6
+ // signal for the inset, three plain variables for the measured frame and the cross-fade setting, a
7
+ // subscribe-at-setup / onCleanup pair over the Keyboard module, and the element assembly around its
8
+ // own children.
9
+ //
10
+ // render-keyboard-avoiding-view.ts deliberately returns a layout DESCRIPTION rather than a
11
+ // Descriptor tree — KAV wraps a live user subtree, which is the case the render-fn boundary rules
12
+ // OUT of core (.claude/rules/component-render-fn-boundary.md). So descriptorToSolid has nothing to
13
+ // do here, exactly as in View and Pressable; the two structural shapes ('nested' vs 'wrapper') are
14
+ // written directly as the two branches at the bottom.
15
+ //
16
+ // NO whenCommitted anywhere: KAV issues no imperative native call. It only reads `onLayout`, and the
17
+ // engine raises the native layout flag itself the moment a listener rides the prop bag — an event
18
+ // cannot arrive before the view exists, so there is no pre-commit window to retry across.
19
+ //
20
+ // NOTHING here destructures `props`. Solid props are getters and a component body runs ONCE; every
21
+ // read below sits inside an accessor, a memo, or an event handler — including
22
+ // `keyboardVerticalOffset`, which is read at EVENT time so a changed offset applies to the next
23
+ // keyboard event without re-subscribing (React re-runs its useEffect for that; here the closure
24
+ // already sees the live value).
25
+ import { createMemo, createSignal, onCleanup, splitProps } from 'solid-js';
26
+ import { computeInset, keyboardAvoidingEventNamesFor, readKeyboardFrame, readLayoutFrame, readPrefersCrossFadeTransitions, resolveKeyboardAvoidingLayout, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
27
+ import { Keyboard, Platform, dlog, } from '@symbiote-native/engine';
28
+ import { View } from './view.jsx';
29
+ // Read by KeyboardAvoidingView itself; everything else (the aria/role aliases, accessibility*,
30
+ // testID, class) forwards onto the wrapper View, which folds them once through
31
+ // resolveAccessibilityProps and widens the vanished-key case with withStableKeys.
32
+ const HANDLED_PROPS = [
33
+ 'behavior',
34
+ 'enabled',
35
+ 'keyboardVerticalOffset',
36
+ 'contentContainerStyle',
37
+ 'style',
38
+ 'onLayout',
39
+ 'children',
40
+ ];
41
+ export function KeyboardAvoidingView(props) {
42
+ const [local, rest] = splitProps(props, HANDLED_PROPS);
43
+ const [inset, setInset] = createSignal(0);
44
+ // Plain variables, not signals: a re-measure alone must not repaint anything. They only feed the
45
+ // NEXT keyboard event's math, which is what re-runs the layout memo. Same shape as React's
46
+ // frameRef/initialHeightRef, Vue's `let frame` and Svelte's non-$state pair.
47
+ let frame;
48
+ let initialHeight;
49
+ // A device accessibility setting, not state: it cannot change mid-session, and nothing should
50
+ // repaint when the promise lands. So a plain `let` rather than a signal — the same reason `frame`
51
+ // above is one. It is read at EVENT time, by which point the microtask has long resolved; a
52
+ // keyboard event that somehow beat it just sees the `false` default, which is the ordinary math.
53
+ //
54
+ // Through the shared reader, not AccessibilityInfo directly: the engine's getter REJECTS on a
55
+ // native error (RN parity), and nothing awaits this call, so a rejection here would surface as an
56
+ // unhandled promise. readPrefersCrossFadeTransitions owns that fallback once for all five adapters
57
+ // and answers false, which is also what a missing module and an Android host answer.
58
+ let prefersCrossFadeTransitions = false;
59
+ void readPrefersCrossFadeTransitions().then(enabled => {
60
+ prefersCrossFadeTransitions = enabled;
61
+ });
62
+ const onShow = (payload) => {
63
+ const keyboard = readKeyboardFrame(payload);
64
+ const offset = local.keyboardVerticalOffset ?? DEFAULT_VERTICAL_OFFSET;
65
+ const next = computeInset(frame, keyboard, offset, {
66
+ behavior: local.behavior,
67
+ // The inset CURRENTLY applied (RN's this.state.bottom). Read from the signal at event time so
68
+ // it is live: in 'height' mode the wrapper is SHRUNK by it, so the next onLayout reports a
69
+ // frame shorter by exactly that much and core adds it back to cancel the shrink. Without it
70
+ // each further keyboard event computes a smaller overlap and the view walks back down under
71
+ // the keyboard. Core gates the correction on 'height'; the other modes ignore the value.
72
+ previousInset: inset(),
73
+ prefersCrossFadeTransitions,
74
+ });
75
+ dlog(`KeyboardAvoidingView show -> inset ${next}`);
76
+ setInset(next);
77
+ };
78
+ const onHide = () => {
79
+ dlog('KeyboardAvoidingView hide -> inset 0');
80
+ setInset(0);
81
+ };
82
+ // TWO listeners, never three, and their names come from the host (RN KeyboardAvoidingView.js):
83
+ // iOS takes the WILL pair so the view rides up with the keyboard animation instead of snapping
84
+ // into place after it; Android has no will-notifications and keeps the DID pair. The change-frame
85
+ // notification is deliberately absent — RN's own comment says an undocked, split or floating iOS
86
+ // keyboard emits it BEFORE the hide, so listening to it applies a frame captured mid-dismissal.
87
+ //
88
+ // Subscribed in the setup body, which runs once per mount — Solid's counterpart to React's
89
+ // mount-effect and Vue's onMounted. onCleanup runs on dispose (render.ts's teardown disposes the
90
+ // whole root), so an unmount removes exactly the listeners this mount added; leaking them would
91
+ // leave a dead closure calling setInset on a disposed signal on every keystroke.
92
+ const events = keyboardAvoidingEventNamesFor(Platform.OS);
93
+ const subscriptions = [
94
+ Keyboard.addListener(events.show, onShow),
95
+ Keyboard.addListener(events.hide, onHide),
96
+ ];
97
+ onCleanup(() => {
98
+ for (const subscription of subscriptions)
99
+ subscription.remove();
100
+ });
101
+ const handleLayout = (event) => {
102
+ const measured = readLayoutFrame(event.nativeEvent.layout);
103
+ if (measured !== undefined) {
104
+ frame = measured;
105
+ if (initialHeight === undefined)
106
+ initialHeight = measured.height;
107
+ }
108
+ local.onLayout?.(event);
109
+ };
110
+ const layout = createMemo(() => resolveKeyboardAvoidingLayout({
111
+ behavior: local.behavior,
112
+ // When disabled the inset is forced to 0, so every behavior mode renders untouched. `inset()`
113
+ // is not read at all on that branch, so a disabled KAV does not even subscribe to the signal;
114
+ // re-enabling re-runs this memo through the `local.enabled` getter and picks the value up.
115
+ effectiveInset: local.enabled === false ? 0 : inset(),
116
+ initialHeight,
117
+ style: local.style,
118
+ contentContainerStyle: local.contentContainerStyle,
119
+ }));
120
+ // What keeps the children alive across a keystroke. Solid has no reconciler between this file and
121
+ // the host nodes — `insert` REPLACES a subtree (.claude/rules/solid-descriptor-bridge.md §4) — and
122
+ // the branch below runs inside the wrapper's `insert` render effect. `layout()` returns a fresh
123
+ // object on every keyboard event, so anything that lets it reach that effect tears the wrapped
124
+ // subtree down and rebuilds it on every show/hide: typically the form the user is typing in, which
125
+ // loses focus mid-keystroke. createMemo's default `===` equality stops the churn at the BOOLEAN —
126
+ // it only notifies when the structure actually flips, i.e. when `behavior` moves in or out of
127
+ // 'position'.
128
+ //
129
+ // babel-preset-solid's `wrapConditionals` (on by default) would memoize the condition of the
130
+ // ternary below on its own, so the inline form happens to be safe too. Naming the memo makes that
131
+ // guarantee local instead of a compiler flag nobody reads, and it survives the refactor that
132
+ // actually breaks it — measured: moving the same branch into a plain `renderContent()` helper (the
133
+ // shape Pressable uses) defeats wrapConditionals and rebuilds the subtree on every keyboard event.
134
+ const isNested = createMemo(() => layout().kind === 'nested');
135
+ // The inset reaches the host as a PROP instead: `spread` runs a per-key diff on the SAME element,
136
+ // so nothing is rebuilt.
137
+ const innerStyle = () => {
138
+ const resolved = layout();
139
+ return resolved.kind === 'nested' ? resolved.innerStyle : undefined;
140
+ };
141
+ const wrapperProps = createMemo(() => ({
142
+ ...rest,
143
+ style: layout().wrapperStyle,
144
+ // Last, so KAV's own measure always runs; the caller's handler is invoked from inside it.
145
+ onLayout: handleLayout,
146
+ }));
147
+ // 'nested' ('position') pushes the children in an inner View by `bottom: inset`; the wrapper modes
148
+ // ('padding' / 'height' / no behavior) adjust the single wrapper directly.
149
+ return (<View {...wrapperProps()}>
150
+ {isNested() ? (<View style={innerStyle()}>{local.children}</View>) : (local.children)}
151
+ </View>);
152
+ }
@@ -0,0 +1,24 @@
1
+ import type { JSX } from '../jsx-runtime';
2
+ import { type IAccessibilityProps, type IAriaProps, type IModalAnimationType, type IModalOrientation, type IModalPresentationStyle } from '@symbiote-native/components';
3
+ import { type IClassNameValue, type IStyleProp, type ISymbioteEvent, type IViewStyle } from '@symbiote-native/engine';
4
+ export type { IModalAnimationType, IModalPresentationStyle, IModalOrientation, IModalOrientationChangeEvent, } from '@symbiote-native/components';
5
+ export interface IModalProps extends IAccessibilityProps, IAriaProps {
6
+ visible?: boolean;
7
+ transparent?: boolean;
8
+ backdropColor?: string;
9
+ animationType?: IModalAnimationType;
10
+ presentationStyle?: IModalPresentationStyle;
11
+ supportedOrientations?: ReadonlyArray<IModalOrientation>;
12
+ hardwareAccelerated?: boolean;
13
+ statusBarTranslucent?: boolean;
14
+ navigationBarTranslucent?: boolean;
15
+ allowSwipeDismissal?: boolean;
16
+ onShow?: () => void;
17
+ onDismiss?: () => void;
18
+ onRequestClose?: () => void;
19
+ onOrientationChange?: (event: ISymbioteEvent) => void;
20
+ style?: IStyleProp<IViewStyle>;
21
+ class?: IClassNameValue;
22
+ children?: JSX.Element;
23
+ }
24
+ export declare function Modal(props: IModalProps): JSX.Element;