@symbiote-native/solid 3.2.0 → 3.4.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 (64) hide show
  1. package/build/components/flat-list/index.d.ts +3 -2
  2. package/build/components/flat-list/index.jsx +8 -10
  3. package/build/components/input-accessory-view-props.d.ts +4 -4
  4. package/build/components/keyboard-avoiding-view.d.ts +2 -2
  5. package/build/components/keyboard-avoiding-view.jsx +34 -114
  6. package/build/components/layout-conformance-props.d.ts +6 -0
  7. package/build/components/layout-conformance-props.js +1 -0
  8. package/build/components/modal.d.ts +4 -4
  9. package/build/components/modal.jsx +11 -11
  10. package/build/components/refresh-control-props.d.ts +7 -7
  11. package/build/components/scroll-view-props.d.ts +8 -5
  12. package/build/components/text-input-props.d.ts +2 -1
  13. package/build/components/text-props.d.ts +3 -3
  14. package/build/components/touchable-props.d.ts +6 -6
  15. package/build/components/virtualized-list/index.android.d.ts +2 -2
  16. package/build/components/virtualized-list/index.ios.d.ts +2 -2
  17. package/build/components/virtualized-list/item-content.d.ts +6 -0
  18. package/build/components/virtualized-list/item-content.jsx +9 -0
  19. package/build/components/virtualized-list/list-body.d.ts +15 -0
  20. package/build/components/virtualized-list/list-body.jsx +20 -0
  21. package/build/components/virtualized-list/list-parts.d.ts +14 -0
  22. package/build/components/virtualized-list/list-parts.js +27 -0
  23. package/build/components/virtualized-list/list-rows.d.ts +18 -0
  24. package/build/components/virtualized-list/list-rows.jsx +91 -0
  25. package/build/components/virtualized-list/list-window.d.ts +11 -0
  26. package/build/components/virtualized-list/list-window.js +29 -0
  27. package/build/components/virtualized-list/nested-scope.d.ts +7 -0
  28. package/build/components/virtualized-list/nested-scope.js +21 -0
  29. package/build/components/virtualized-list/scroll-handlers.d.ts +12 -0
  30. package/build/components/virtualized-list/scroll-handlers.js +31 -0
  31. package/build/components/virtualized-list/scroll-tree.d.ts +18 -0
  32. package/build/components/virtualized-list/scroll-tree.jsx +143 -0
  33. package/build/components/virtualized-list/shared.d.ts +3 -74
  34. package/build/components/virtualized-list/shared.jsx +53 -752
  35. package/build/components/virtualized-list/sticky-binding.d.ts +13 -0
  36. package/build/components/virtualized-list/sticky-binding.js +111 -0
  37. package/build/components/virtualized-list/sticky-header.d.ts +1 -0
  38. package/build/components/virtualized-list/sticky-header.jsx +11 -159
  39. package/build/components/virtualized-list/sticky-state.d.ts +14 -0
  40. package/build/components/virtualized-list/sticky-state.js +54 -0
  41. package/build/components/virtualized-list/use-list-driver.d.ts +22 -0
  42. package/build/components/virtualized-list/use-list-driver.js +128 -0
  43. package/build/components/virtualized-list/use-separators.d.ts +7 -0
  44. package/build/components/virtualized-list/use-separators.js +22 -0
  45. package/build/components/virtualized-list/virtualized-list-props.d.ts +86 -0
  46. package/build/components/virtualized-list/virtualized-list-props.js +53 -0
  47. package/build/components/virtualized-section-list/index.d.ts +22 -9
  48. package/build/components/virtualized-section-list/index.jsx +87 -115
  49. package/build/components/virtualized-section-list/section-item-cell.d.ts +18 -0
  50. package/build/components/virtualized-section-list/section-item-cell.jsx +37 -0
  51. package/build/components.d.ts +5 -1
  52. package/build/components.js +1 -0
  53. package/build/index.d.ts +10 -4
  54. package/build/index.js +21 -37
  55. package/build/modules/animated/index.d.ts +54 -41
  56. package/build/modules/animated/index.js +2 -26
  57. package/build/modules/app-registry/index.d.ts +3 -3
  58. package/build/modules/app-registry/index.js +2 -2
  59. package/build/modules/status-bar.js +14 -27
  60. package/build/primitives/create-color-scheme.d.ts +1 -1
  61. package/build/render.js +16 -28
  62. package/build/root-tag-context.d.ts +1 -0
  63. package/build/root-tag-context.js +4 -0
  64. package/package.json +6 -6
@@ -1,9 +1,10 @@
1
1
  import type { JSX } from '../../jsx-runtime';
2
2
  import { type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
3
- import { type IVirtualizedListHandle, type IVirtualizedListProps } from '../virtualized-list';
3
+ import { type ICellRendererComponent, type IVirtualizedListHandle, type IVirtualizedListProps } from '../virtualized-list';
4
4
  export type IFlatListHandle = IVirtualizedListHandle;
5
- export type IFlatListProps<ItemT> = Omit<IVirtualizedListProps<ItemT>, 'data' | 'getItem' | 'getItemCount'> & {
5
+ export type IFlatListProps<ItemT> = Omit<IVirtualizedListProps<ItemT>, 'data' | 'getItem' | 'getItemCount' | 'CellRendererComponent'> & {
6
6
  data: readonly ItemT[];
7
+ CellRendererComponent?: ICellRendererComponent<unknown>;
7
8
  numColumns?: number;
8
9
  columnWrapperStyle?: IStyleProp<IViewStyle> | string;
9
10
  };
@@ -25,8 +25,9 @@
25
25
  // caller receives that list's own handle — FlatList's handle IS VirtualizedList's (RN says the
26
26
  // same).
27
27
  import { Index, Show, createMemo, splitProps, untrack, } from 'solid-js';
28
- import { SINGLE_COLUMN, chunkIntoRows, expandRowViewability, firstItemOfRow, lastItemOfRow, removeClippedSubviewsOrDefault, rowKeyExtractor, } from '@symbiote-native/components';
28
+ import { SINGLE_COLUMN, arrayLikeLength, chunkIntoRows, expandRowViewability, firstItemOfRow, lastItemOfRow, removeClippedSubviewsOrDefault, rowKeyExtractor, } from '@symbiote-native/components';
29
29
  import { Platform, dlog, resolveClassName, } from '@symbiote-native/engine';
30
+ import { renderItemContent } from '../virtualized-list/item-content.jsx';
30
31
  import { VirtualizedList, } from '../virtualized-list';
31
32
  // What FlatList itself consumes or re-shapes. Everything NOT listed here is passthrough and rides
32
33
  // down onto the inner list untouched — `ref` among it, which is why no delegate handle is needed.
@@ -35,6 +36,7 @@ const OWN_PROPS = [
35
36
  'numColumns',
36
37
  'columnWrapperStyle',
37
38
  'renderItem',
39
+ 'ListItemComponent',
38
40
  'keyExtractor',
39
41
  'ItemSeparatorComponent',
40
42
  'onViewableItemsChanged',
@@ -56,7 +58,7 @@ export function FlatList(props) {
56
58
  const columns = columnCount();
57
59
  // Logged from inside the memo, not from the body: a Solid body runs ONCE, so a line emitted
58
60
  // there would report the mount-time shape forever and be actively misleading on a data change.
59
- dlog(`Solid FlatList over ${props.data.length} items, ${columns} column(s)`);
61
+ dlog(`Solid FlatList over ${arrayLikeLength(props.data)} items, ${columns} column(s)`);
60
62
  return columns > SINGLE_COLUMN ? chunkIntoRows(props.data, columns) : [];
61
63
  });
62
64
  // A bare string is a registered class name and has to be resolved here — the row view's style
@@ -74,7 +76,7 @@ export function FlatList(props) {
74
76
  // Called ONCE and untracked, the shape VirtualizedList's own cell build uses: a
75
77
  // tracked call would put every signal renderItem reads into this column's insert
76
78
  // effect and rebuild the subtree instead of updating its leaf.
77
- const content = untrack(() => props.renderItem(() => ({
79
+ const content = untrack(() => renderItemContent(props, () => ({
78
80
  item: item(),
79
81
  index: info().item.startIndex + column,
80
82
  // The row IS the virtualized cell, so every column in it shares the row's
@@ -108,13 +110,9 @@ export function FlatList(props) {
108
110
  pair.onViewableItemsChanged(expandRowViewability(rowInfo, props.keyExtractor));
109
111
  },
110
112
  })));
111
- // The two paths instantiate VirtualizedList over DIFFERENT item types — ItemT vs IRow<ItemT> —
112
- // so they are two elements, not one with switching props: a single instantiation would have to
113
- // union the two and narrow ItemT | IRow<ItemT> back apart at runtime, which nothing can do
114
- // soundly for an arbitrary ItemT. <Show> memoizes on the flip alone, so a numColumns change
115
- // rebuilds (RN documents changing it live as unsupported — "change the key prop" — which is the
116
- // same fresh start) while every other prop change re-props the live list.
117
- return (<Show when={isMultiColumn()} fallback={<VirtualizedList {...passthrough} removeClippedSubviews={removeClippedSubviews()} data={props.data} getItem={(_source, index) => props.data[index]} getItemCount={() => props.data.length} renderItem={props.renderItem} keyExtractor={props.keyExtractor} ItemSeparatorComponent={props.ItemSeparatorComponent} onViewableItemsChanged={props.onViewableItemsChanged} viewabilityConfigCallbackPairs={props.viewabilityConfigCallbackPairs}/>}>
113
+ // Два списка с разными типами элементов, `ItemT` и `IRow<ItemT>`, одним элементом их не выразить
114
+ // `<Show>` пересобирает список только при смене `numColumns`, остальные props обновляют живой
115
+ return (<Show when={isMultiColumn()} fallback={<VirtualizedList {...passthrough} removeClippedSubviews={removeClippedSubviews()} data={props.data} getItem={(_source, index) => props.data[index]} getItemCount={() => arrayLikeLength(props.data)} renderItem={props.renderItem} ListItemComponent={props.ListItemComponent} keyExtractor={props.keyExtractor} ItemSeparatorComponent={props.ItemSeparatorComponent} onViewableItemsChanged={props.onViewableItemsChanged} viewabilityConfigCallbackPairs={props.viewabilityConfigCallbackPairs}/>}>
118
116
  <VirtualizedList {...passthrough} removeClippedSubviews={removeClippedSubviews()} data={rows()} getItem={(_source, index) => rows()[index]} getItemCount={() => rows().length} renderItem={renderRow} keyExtractor={(row) => rowKeyExtractor(row, props.keyExtractor)} ItemSeparatorComponent={rowSeparatorComponent()} onViewableItemsChanged={rowViewability()} viewabilityConfigCallbackPairs={rowViewabilityPairs()}/>
119
117
  </Show>);
120
118
  }
@@ -1,10 +1,10 @@
1
1
  import type { JSX } from '../jsx-runtime';
2
2
  import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
3
- import type { IClassNameValue, IStyleProp, IViewStyle } from '@symbiote-native/engine';
4
- export interface IInputAccessoryViewProps extends IAccessibilityProps, IAriaProps {
3
+ import type { IClassNameValue, IColorValue, IStyleProp, IViewStyle } from '@symbiote-native/engine';
4
+ export type IInputAccessoryViewProps = IAccessibilityProps & IAriaProps & {
5
5
  nativeID?: string;
6
- backgroundColor?: string;
6
+ backgroundColor?: IColorValue;
7
7
  style?: IStyleProp<IViewStyle>;
8
8
  class?: IClassNameValue;
9
9
  children?: JSX.Element;
10
- }
10
+ };
@@ -2,7 +2,7 @@ import type { JSX } from '../jsx-runtime';
2
2
  import { type IAccessibilityProps, type IAriaProps, type IKeyboardAvoidingBehavior } from '@symbiote-native/components';
3
3
  import { type IClassNameValue, type IStyleProp, type ISymbioteEvent, type IViewStyle } from '@symbiote-native/engine';
4
4
  export type { IKeyboardAvoidingBehavior } from '@symbiote-native/components';
5
- export interface IKeyboardAvoidingViewProps extends IAccessibilityProps, IAriaProps {
5
+ export type IKeyboardAvoidingViewProps = IAccessibilityProps & IAriaProps & {
6
6
  behavior?: IKeyboardAvoidingBehavior;
7
7
  enabled?: boolean;
8
8
  keyboardVerticalOffset?: number;
@@ -11,5 +11,5 @@ export interface IKeyboardAvoidingViewProps extends IAccessibilityProps, IAriaPr
11
11
  class?: IClassNameValue;
12
12
  onLayout?: (event: ISymbioteEvent) => void;
13
13
  children?: JSX.Element;
14
- }
14
+ };
15
15
  export declare function KeyboardAvoidingView(props: IKeyboardAvoidingViewProps): JSX.Element;
@@ -1,34 +1,10 @@
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).
1
+ // KeyboardAvoidingView, the Solid lifecycle half over the shared `createKeyboardAvoidingModel`
2
+ // Nothing destructures `props`: they are getters, read at event time or inside an accessor
25
3
  import { createMemo, createSignal, onCleanup, splitProps } from 'solid-js';
26
- import { computeInset, configureKeyboardAvoidingAnimation, keyboardAvoidingEventNamesFor, readKeyboardAnimationTiming, readKeyboardFrame, readLayoutFrame, readPrefersCrossFadeTransitions, resolveKeyboardAvoidingLayout, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
27
- import { Keyboard, Platform, dlog, } from '@symbiote-native/engine';
4
+ import { createKeyboardAvoidingModel, keyboardAvoidingEventNamesFor, readPrefersCrossFadeTransitions, resolveKeyboardAvoidingLayout, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
5
+ import { Keyboard, Platform, } from '@symbiote-native/engine';
28
6
  import { withStableKeys } from '../utils/stable-keys.js';
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.
7
+ // Read here, the rest forwards onto the wrapper which folds aria/role and accessibility* once
32
8
  const HANDLED_PROPS = [
33
9
  'behavior',
34
10
  'enabled',
@@ -38,106 +14,55 @@ const HANDLED_PROPS = [
38
14
  'onLayout',
39
15
  'children',
40
16
  ];
41
- export function KeyboardAvoidingView(props) {
42
- const [local, rest] = splitProps(props, HANDLED_PROPS);
17
+ // The model with its inset signal and keyboard subscriptions, kept apart so the component reads as
18
+ // the element assembly
19
+ function createKeyboardModel(props) {
43
20
  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.
21
+ // A device setting, not state: nothing should repaint when the promise lands
58
22
  let prefersCrossFadeTransitions = false;
59
23
  void readPrefersCrossFadeTransitions().then(enabled => {
60
24
  prefersCrossFadeTransitions = enabled;
61
25
  });
62
- const onShow = (payload) => {
63
- const keyboard = readKeyboardFrame(payload);
64
- const offset = local.keyboardVerticalOffset ?? DEFAULT_VERTICAL_OFFSET;
65
- const previousInset = inset();
66
- const next = computeInset(frame, keyboard, offset, {
67
- behavior: local.behavior,
68
- // The inset CURRENTLY applied (RN's this.state.bottom). Read from the signal at event time so
69
- // it is live: in 'height' mode the wrapper is SHRUNK by it, so the next onLayout reports a
70
- // frame shorter by exactly that much and core adds it back to cancel the shrink. Without it
71
- // each further keyboard event computes a smaller overlap and the view walks back down under
72
- // the keyboard. Core gates the correction on 'height'; the other modes ignore the value.
73
- previousInset,
74
- prefersCrossFadeTransitions,
75
- });
76
- // RN's `_updateBottomIfNecessary` skips the animation when the inset did not change.
77
- if (next !== previousInset)
78
- configureKeyboardAvoidingAnimation(readKeyboardAnimationTiming(payload), local.enabled !== false);
79
- dlog(`KeyboardAvoidingView show -> inset ${next}`);
80
- setInset(next);
81
- };
82
- const onHide = () => {
83
- dlog('KeyboardAvoidingView hide -> inset 0');
84
- setInset(0);
85
- };
86
- // TWO listeners, never three, and their names come from the host (RN KeyboardAvoidingView.js):
87
- // iOS takes the WILL pair so the view rides up with the keyboard animation instead of snapping
88
- // into place after it; Android has no will-notifications and keeps the DID pair. The change-frame
89
- // notification is deliberately absent — RN's own comment says an undocked, split or floating iOS
90
- // keyboard emits it BEFORE the hide, so listening to it applies a frame captured mid-dismissal.
91
- //
92
- // Subscribed in the setup body, which runs once per mount — Solid's counterpart to React's
93
- // mount-effect and Vue's onMounted. onCleanup runs on dispose (render.ts's teardown disposes the
94
- // whole root), so an unmount removes exactly the listeners this mount added; leaking them would
95
- // leave a dead closure calling setInset on a disposed signal on every keystroke.
26
+ const model = createKeyboardAvoidingModel({
27
+ options: () => ({
28
+ behavior: props.behavior,
29
+ enabled: props.enabled !== false,
30
+ keyboardVerticalOffset: props.keyboardVerticalOffset ?? DEFAULT_VERTICAL_OFFSET,
31
+ }),
32
+ setInset,
33
+ prefersCrossFade: () => prefersCrossFadeTransitions,
34
+ });
35
+ // Subscribed in the setup body, which runs once per mount, and removed when the root is disposed
96
36
  const events = keyboardAvoidingEventNamesFor(Platform.OS);
97
37
  const subscriptions = [
98
- Keyboard.addListener(events.show, onShow),
99
- Keyboard.addListener(events.hide, onHide),
38
+ Keyboard.addListener(events.show, model.keyboardShown),
39
+ Keyboard.addListener(events.hide, model.keyboardHidden),
100
40
  ];
101
41
  onCleanup(() => {
102
42
  for (const subscription of subscriptions)
103
43
  subscription.remove();
104
44
  });
45
+ return { inset, model };
46
+ }
47
+ export function KeyboardAvoidingView(props) {
48
+ const [local, rest] = splitProps(props, HANDLED_PROPS);
49
+ const { inset, model } = createKeyboardModel(props);
105
50
  const handleLayout = (event) => {
106
- const measured = readLayoutFrame(event.nativeEvent.layout);
107
- if (measured !== undefined) {
108
- frame = measured;
109
- if (initialHeight === undefined)
110
- initialHeight = measured.height;
111
- }
51
+ model.laidOut(event.nativeEvent.layout);
112
52
  local.onLayout?.(event);
113
53
  };
114
54
  const layout = createMemo(() => resolveKeyboardAvoidingLayout({
115
55
  behavior: local.behavior,
116
- // When disabled the inset is forced to 0, so every behavior mode renders untouched. `inset()`
117
- // is not read at all on that branch, so a disabled KAV does not even subscribe to the signal;
118
- // re-enabling re-runs this memo through the `local.enabled` getter and picks the value up.
56
+ // `inset()` is not read when disabled, so a disabled view does not subscribe to the signal
119
57
  effectiveInset: local.enabled === false ? 0 : inset(),
120
- initialHeight,
58
+ initialHeight: model.initialHeight(),
121
59
  style: local.style,
122
60
  contentContainerStyle: local.contentContainerStyle,
123
61
  }));
124
- // What keeps the children alive across a keystroke. Solid has no reconciler between this file and
125
- // the host nodes — `insert` REPLACES a subtree (.claude/rules/solid-descriptor-bridge.md §4) — and
126
- // the branch below runs inside the wrapper's `insert` render effect. `layout()` returns a fresh
127
- // object on every keyboard event, so anything that lets it reach that effect tears the wrapped
128
- // subtree down and rebuilds it on every show/hide: typically the form the user is typing in, which
129
- // loses focus mid-keystroke. createMemo's default `===` equality stops the churn at the BOOLEAN —
130
- // it only notifies when the structure actually flips, i.e. when `behavior` moves in or out of
131
- // 'position'.
132
- //
133
- // babel-preset-solid's `wrapConditionals` (on by default) would memoize the condition of the
134
- // ternary below on its own, so the inline form happens to be safe too. Naming the memo makes that
135
- // guarantee local instead of a compiler flag nobody reads, and it survives the refactor that
136
- // actually breaks it — measured: moving the same branch into a plain `renderContent()` helper (the
137
- // shape Pressable uses) defeats wrapConditionals and rebuilds the subtree on every keyboard event.
62
+ // Named so the children survive a keystroke: `layout()` is a fresh object per keyboard event,
63
+ // and this boolean only notifies when the structure flips, `insert` would rebuild the subtree
138
64
  const isNested = createMemo(() => layout().kind === 'nested');
139
- // The inset reaches the host as a PROP instead: `spread` runs a per-key diff on the SAME element,
140
- // so nothing is rebuilt.
65
+ // The inset reaches the host as a prop, `spread` diffs it on the same element
141
66
  const innerStyle = () => {
142
67
  const resolved = layout();
143
68
  return resolved.kind === 'nested' ? resolved.innerStyle : undefined;
@@ -145,16 +70,11 @@ export function KeyboardAvoidingView(props) {
145
70
  const wrapperProps = createMemo(() => ({
146
71
  ...rest,
147
72
  style: layout().wrapperStyle,
148
- // Last, so KAV's own measure always runs; the caller's handler is invoked from inside it.
73
+ // Last, so the model always measures, the caller's handler is invoked from inside
149
74
  onLayout: handleLayout,
150
75
  }));
151
- // The half `View` used to supply, now that the wrapper is a bare tag: `spread` has no removal
152
- // pass, so a key that stops being emitted (`rest` is a splitProps proxy, and `wrapperStyle` is
153
- // absent in some behaviors) would keep its last value on the native view forever
154
- // (.claude/rules/solid-descriptor-bridge.md §1).
76
+ // `spread` has no removal pass, so a key that stops being emitted would keep its last value
155
77
  const stableWrapperProps = withStableKeys(wrapperProps);
156
- // 'nested' ('position') pushes the children in an inner View by `bottom: inset`; the wrapper modes
157
- // ('padding' / 'height' / no behavior) adjust the single wrapper directly.
158
78
  return (<view {...stableWrapperProps()}>
159
79
  {isNested() ? (<view style={innerStyle()}>{local.children}</view>) : (local.children)}
160
80
  </view>);
@@ -0,0 +1,6 @@
1
+ import type { ILayoutConformanceMode } from '@symbiote-native/components';
2
+ import type { JSX } from '../jsx-runtime';
3
+ export type ILayoutConformanceProps = {
4
+ mode: ILayoutConformanceMode;
5
+ children?: JSX.Element;
6
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,11 +1,11 @@
1
1
  import type { JSX } from '../jsx-runtime';
2
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';
3
+ import { type IClassNameValue, type IColorValue, type IStyleProp, type ISymbioteEvent, type IViewStyle } from '@symbiote-native/engine';
4
4
  export type { IModalAnimationType, IModalPresentationStyle, IModalOrientation, IModalOrientationChangeEvent, } from '@symbiote-native/components';
5
- export interface IModalProps extends IAccessibilityProps, IAriaProps {
5
+ export type IModalProps = IAccessibilityProps & IAriaProps & {
6
6
  visible?: boolean;
7
7
  transparent?: boolean;
8
- backdropColor?: string;
8
+ backdropColor?: IColorValue;
9
9
  animationType?: IModalAnimationType;
10
10
  presentationStyle?: IModalPresentationStyle;
11
11
  supportedOrientations?: ReadonlyArray<IModalOrientation>;
@@ -20,5 +20,5 @@ export interface IModalProps extends IAccessibilityProps, IAriaProps {
20
20
  style?: IStyleProp<IViewStyle>;
21
21
  class?: IClassNameValue;
22
22
  children?: JSX.Element;
23
- }
23
+ };
24
24
  export declare function Modal(props: IModalProps): JSX.Element;
@@ -34,9 +34,10 @@
34
34
  // SEED, which is a mount-time value by definition (React's useReducer lazy init and Svelte's
35
35
  // $state(...) initializer read it exactly once too).
36
36
  import { createEffect, createSignal, Show, splitProps } from 'solid-js';
37
- import { createDescriptorShapeGuard, createInitialModalState, isModalVisible, modalReducer, modalVisibilityAction, renderModal, resolveAccessibilityProps, shouldRenderModal, } from '@symbiote-native/components';
37
+ import { createDescriptorShapeGuard, createInitialModalState, isModalVisible, modalReducer, modalVisibilityAction, renderModal, resolveAccessibilityProps, shouldRenderModal, warnAboutModalProps, } from '@symbiote-native/components';
38
38
  import { dlog, Platform, } from '@symbiote-native/engine';
39
39
  import { withStableKeys } from '../utils/stable-keys.js';
40
+ import { VirtualizedListScopeResetter } from './virtualized-list/nested-scope.js';
40
41
  // Read by Modal itself; everything else (the four DirectEvents, testID/nativeID, every
41
42
  // accessibility* and aria-* field) forwards onto the host node through `passthrough`, exactly as
42
43
  // React's `...passthrough` rest does.
@@ -58,6 +59,8 @@ const HANDLED_PROPS = [
58
59
  ];
59
60
  const shape = createDescriptorShapeGuard('Modal');
60
61
  export function Modal(props) {
62
+ // RN checks on mount and on every update, a dev build only
63
+ createEffect(() => warnAboutModalProps(props));
61
64
  const [local, rest] = splitProps(props, HANDLED_PROPS);
62
65
  const isVisible = () => isModalVisible(local.visible);
63
66
  // The keep-alive seed is a mount-time value by contract (createInitialModalState: a modal that
@@ -83,11 +86,7 @@ export function Modal(props) {
83
86
  if (!shouldRender())
84
87
  dlog('Modal hidden -> no node committed');
85
88
  });
86
- // A plain accessor rather than a createMemo, unlike descriptor-to-solid.ts. A memo runs EAGERLY
87
- // at creation, which would paint a descriptor (and emit renderModal's "committing" dlog) for a
88
- // modal that is hidden and commits nothing. Read from inside the two `spread` render effects
89
- // instead, it never runs at all while `Show` is closed; the cost is that a change recomputes this
90
- // pure object literal twice, once per bag.
89
+ // An accessor, not a memo: a memo runs eagerly and would build a descriptor for a hidden modal
91
90
  const descriptor = () => renderModal({
92
91
  visible: local.visible,
93
92
  transparent: local.transparent,
@@ -113,10 +112,7 @@ export function Modal(props) {
113
112
  throw shape.error('the container child disappeared');
114
113
  return shape.asElement(first);
115
114
  };
116
- // withStableKeys on both bags because `passthrough` goes through resolveAccessibilityProps, whose
117
- // two branches emit DIFFERENT key sets — and Solid's `spread` walks only the CURRENT keys with no
118
- // removal pass, so a vanished key would keep its last value on the native view forever
119
- // (.claude/rules/solid-descriptor-bridge.md §1).
115
+ // `resolveAccessibilityProps` emits different key sets per branch and `spread` never removes one
120
116
  const hostBag = withStableKeys(() => descriptor().props);
121
117
  const containerBag = withStableKeys(() => ({
122
118
  ...container().props,
@@ -124,7 +120,11 @@ export function Modal(props) {
124
120
  }));
125
121
  return (<Show when={shouldRender()}>
126
122
  <modal {...hostBag()}>
127
- <view {...containerBag()}>{local.children}</view>
123
+ <view {...containerBag()}>
124
+ <VirtualizedListScopeResetter>
125
+ {local.children}
126
+ </VirtualizedListScopeResetter>
127
+ </view>
128
128
  </modal>
129
129
  </Show>);
130
130
  }
@@ -1,18 +1,18 @@
1
1
  import type { JSX } from '../jsx-runtime';
2
2
  import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
3
- import type { IClassNameValue } from '@symbiote-native/engine';
4
- export interface IRefreshControlProps extends IAccessibilityProps, IAriaProps {
3
+ import type { IClassNameValue, IColorValue } from '@symbiote-native/engine';
4
+ export type IRefreshControlProps = IAccessibilityProps & IAriaProps & {
5
5
  refreshing: boolean;
6
6
  id?: string;
7
7
  onRefresh?: () => void | Promise<void>;
8
- tintColor?: string;
8
+ tintColor?: IColorValue;
9
9
  title?: string;
10
- titleColor?: string;
10
+ titleColor?: IColorValue;
11
11
  progressViewOffset?: number;
12
- colors?: readonly string[];
13
- progressBackgroundColor?: string;
12
+ colors?: readonly IColorValue[];
13
+ progressBackgroundColor?: IColorValue;
14
14
  size?: 'default' | 'large';
15
15
  enabled?: boolean;
16
16
  class?: IClassNameValue;
17
17
  children?: JSX.Element;
18
- }
18
+ };
@@ -1,12 +1,12 @@
1
1
  import type { Ref } from 'solid-js';
2
2
  import type { JSX } from '../jsx-runtime';
3
3
  import type { ISymbioteEvent } from '@symbiote-native/engine';
4
- import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
5
- import type { IClassNameValue, IStyleProp, IViewStyle } from '@symbiote-native/engine';
4
+ import type { IAccessibilityProps, IAriaProps, IInnerViewRef, IScrollViewNativeOnlyProps } from '@symbiote-native/components';
5
+ import type { IClassNameValue, IColorValue, IStyleProp, IViewStyle } from '@symbiote-native/engine';
6
6
  import type { IHostInstance } from '../host-instance';
7
7
  export type { IScrollViewHandle } from '@symbiote-native/components';
8
8
  type IScrollHandler = (event: ISymbioteEvent) => void;
9
- export interface IScrollViewProps extends IAccessibilityProps, IAriaProps {
9
+ export type IScrollViewProps = IAccessibilityProps & IAriaProps & IScrollViewNativeOnlyProps & {
10
10
  style?: IStyleProp<IViewStyle>;
11
11
  class?: IClassNameValue;
12
12
  contentContainerStyle?: IStyleProp<IViewStyle> | string;
@@ -36,8 +36,11 @@ export interface IScrollViewProps extends IAccessibilityProps, IAriaProps {
36
36
  snapToStart?: boolean;
37
37
  snapToEnd?: boolean;
38
38
  disableIntervalMomentum?: boolean;
39
+ experimental_endDraggingSensitivityMultiplier?: number;
39
40
  stickyHeaderIndices?: number[];
40
41
  invertStickyHeaders?: boolean;
42
+ stickyHeaderHiddenOnScroll?: boolean;
43
+ innerViewRef?: IInnerViewRef;
41
44
  keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
42
45
  keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
43
46
  maintainVisibleContentPosition?: {
@@ -66,7 +69,7 @@ export interface IScrollViewProps extends IAccessibilityProps, IAriaProps {
66
69
  overScrollMode?: 'auto' | 'always' | 'never';
67
70
  fadingEdgeLength?: number;
68
71
  persistentScrollbar?: boolean;
69
- endFillColor?: string;
72
+ endFillColor?: IColorValue;
70
73
  onLayout?: IScrollHandler;
71
74
  onScroll?: IScrollHandler;
72
75
  onScrollBeginDrag?: IScrollHandler;
@@ -76,4 +79,4 @@ export interface IScrollViewProps extends IAccessibilityProps, IAriaProps {
76
79
  onScrollToTop?: IScrollHandler;
77
80
  ref?: Ref<IHostInstance>;
78
81
  children?: JSX.Element;
79
- }
82
+ };
@@ -1,8 +1,9 @@
1
- import type { Ref } from 'solid-js';
1
+ import type { JSX, Ref } from 'solid-js';
2
2
  import type { ITextInputProps as ITextInputBaseProps } from '@symbiote-native/components';
3
3
  import type { IClassNameValue } from '@symbiote-native/engine';
4
4
  import type { IHostInstance } from '../host-instance';
5
5
  export type ITextInputProps = ITextInputBaseProps & {
6
6
  class?: IClassNameValue;
7
7
  ref?: Ref<IHostInstance>;
8
+ children?: JSX.Element;
8
9
  };
@@ -1,9 +1,9 @@
1
1
  import type { Ref } from 'solid-js';
2
2
  import type { JSX } from '../jsx-runtime';
3
- import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
3
+ import type { IAccessibilityProps, IAriaProps, ITextNativeOnlyProps } from '@symbiote-native/components';
4
4
  import type { IClassNameValue, IStyleProp, ISymbioteEvent, ITextStyle } from '@symbiote-native/engine';
5
5
  import type { IHostInstance } from '../host-instance';
6
- export interface ITextProps extends IAccessibilityProps, IAriaProps {
6
+ export type ITextProps = IAccessibilityProps & IAriaProps & ITextNativeOnlyProps & {
7
7
  style?: IStyleProp<ITextStyle>;
8
8
  class?: IClassNameValue;
9
9
  onPress?: (event: ISymbioteEvent) => void;
@@ -23,4 +23,4 @@ export interface ITextProps extends IAccessibilityProps, IAriaProps {
23
23
  selectionColor?: string;
24
24
  ref?: Ref<IHostInstance>;
25
25
  children?: JSX.Element;
26
- }
26
+ };
@@ -1,20 +1,20 @@
1
1
  import type { JSX } from '../jsx-runtime';
2
2
  import type { IPressTimingProps } from '@symbiote-native/components';
3
- import type { IClassNameValue, IStyleProp, IViewStyle } from '@symbiote-native/engine';
3
+ import type { IClassNameValue, IColorValue, IStyleProp, IViewStyle } from '@symbiote-native/engine';
4
4
  import type { IPressableProps } from './pressable-props';
5
5
  export type ITouchableBaseProps = Omit<IPressableProps, 'style' | 'class' | 'children'> & IPressTimingProps & {
6
6
  style?: IStyleProp<IViewStyle>;
7
7
  class?: IClassNameValue;
8
8
  children?: JSX.Element;
9
9
  };
10
- export interface ITouchableOpacityProps extends ITouchableBaseProps {
10
+ export type ITouchableOpacityProps = ITouchableBaseProps & {
11
11
  activeOpacity?: number;
12
- }
13
- export interface ITouchableHighlightProps extends ITouchableBaseProps {
12
+ };
13
+ export type ITouchableHighlightProps = ITouchableBaseProps & {
14
14
  activeOpacity?: number;
15
- underlayColor?: string;
15
+ underlayColor?: IColorValue;
16
16
  touchSoundDisabled?: boolean;
17
17
  testOnly_pressed?: boolean;
18
18
  onShowUnderlay?: () => void;
19
19
  onHideUnderlay?: () => void;
20
- }
20
+ };
@@ -1,2 +1,2 @@
1
- export type { IVirtualizedListProps, IVirtualizedListComponent, IVirtualizedListCellInfo, IVirtualizedListRenderItem, IVirtualizedListHandle, ICellLayout, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, } from './shared';
2
- export declare const VirtualizedList: import("./shared").IVirtualizedListComponent;
1
+ export type { IVirtualizedListProps, IVirtualizedListComponent, ICellRendererComponent, ICellRendererProps, IVirtualizedListCellInfo, IVirtualizedListRenderItem, IVirtualizedListHandle, ICellLayout, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, } from './shared';
2
+ export declare const VirtualizedList: import("./virtualized-list-props").IVirtualizedListComponent;
@@ -1,2 +1,2 @@
1
- export type { IVirtualizedListProps, IVirtualizedListComponent, IVirtualizedListCellInfo, IVirtualizedListRenderItem, IVirtualizedListHandle, ICellLayout, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, } from './shared';
2
- export declare const VirtualizedList: import("./shared").IVirtualizedListComponent;
1
+ export type { IVirtualizedListProps, IVirtualizedListComponent, ICellRendererComponent, ICellRendererProps, IVirtualizedListCellInfo, IVirtualizedListRenderItem, IVirtualizedListHandle, ICellLayout, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, } from './shared';
2
+ export declare const VirtualizedList: import("./virtualized-list-props").IVirtualizedListComponent;
@@ -0,0 +1,6 @@
1
+ import type { Accessor } from 'solid-js';
2
+ import type { JSX } from '../../jsx-runtime';
3
+ import type { IVirtualizedListCellInfo, IVirtualizedListProps } from './virtualized-list-props';
4
+ type IItemRenderers<ItemT> = Pick<IVirtualizedListProps<ItemT>, 'renderItem' | 'ListItemComponent'>;
5
+ export declare function renderItemContent<ItemT>(renderers: IItemRenderers<ItemT>, info: Accessor<IVirtualizedListCellInfo<ItemT>>): JSX.Element;
6
+ export {};
@@ -0,0 +1,9 @@
1
+ // One cell's content: `ListItemComponent` or `renderItem`, RN's `_renderElement`
2
+ import { drawItem } from '@symbiote-native/components';
3
+ // The component takes the info as plain props, reactive getters over the accessor
4
+ export function renderItemContent(renderers, info) {
5
+ return drawItem({
6
+ renderItem: renderers.renderItem,
7
+ component: renderers.ListItemComponent,
8
+ }, info, (Component, given) => (<Component item={given().item} index={given().index} separators={given().separators}/>));
9
+ }
@@ -0,0 +1,15 @@
1
+ import { type Accessor } from 'solid-js';
2
+ import type { IStyleProp, IViewStyle } from '@symbiote-native/engine';
3
+ import type { JSX } from '../../jsx-runtime';
4
+ export type IListBodyDeps = {
5
+ header: JSX.Element;
6
+ footer: JSX.Element;
7
+ empty: JSX.Element;
8
+ headerStyle: () => IStyleProp<IViewStyle> | undefined;
9
+ footerStyle: () => IStyleProp<IViewStyle> | undefined;
10
+ emptyStyle: () => IStyleProp<IViewStyle> | undefined;
11
+ isEmpty: () => boolean;
12
+ rowKeys: Accessor<string[]>;
13
+ renderRow: (rowKey: string) => JSX.Element;
14
+ };
15
+ export declare function listBody(deps: IListBodyDeps): JSX.Element;
@@ -0,0 +1,20 @@
1
+ // The children of the content node: header, the keyed rows (or the empty slot), footer
2
+ import { For, Show } from 'solid-js';
3
+ // Every reactive read sits inside a `<Show>` or `<For>` prop getter, so this function has no
4
+ // dependencies and the content view's `insert` effect runs exactly once, a condition moved into a
5
+ // plain helper called from here would rebuild the whole list on the next scroll
6
+ export function listBody(deps) {
7
+ return [
8
+ <Show when={deps.header !== undefined}>
9
+ <view style={deps.headerStyle()}>{deps.header}</view>
10
+ </Show>,
11
+ <Show when={deps.isEmpty()} fallback={<For each={deps.rowKeys()}>{deps.renderRow}</For>}>
12
+ <Show when={deps.empty !== undefined}>
13
+ <view style={deps.emptyStyle()}>{deps.empty}</view>
14
+ </Show>
15
+ </Show>,
16
+ <Show when={deps.footer !== undefined}>
17
+ <view style={deps.footerStyle()}>{deps.footer}</view>
18
+ </Show>,
19
+ ];
20
+ }
@@ -0,0 +1,14 @@
1
+ import { type IListNesting } from '@symbiote-native/components';
2
+ import { createListWindow } from './list-window';
3
+ import { type IScrollHandlers } from './scroll-handlers';
4
+ import { createStickyState } from './sticky-state';
5
+ import { type IListDriver } from './use-list-driver';
6
+ import type { IVirtualizedListProps } from './virtualized-list-props';
7
+ export type IListParts<ItemT> = {
8
+ driver: IListDriver<ItemT>;
9
+ nesting: IListNesting<ItemT>;
10
+ sticky: ReturnType<typeof createStickyState>;
11
+ listWindow: ReturnType<typeof createListWindow>;
12
+ handlers: IScrollHandlers;
13
+ };
14
+ export declare function createListParts<ItemT>(props: IVirtualizedListProps<ItemT>): IListParts<ItemT>;