@symbiote-native/components 0.4.0 → 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.
- package/README.md +30 -8
- package/build/accessibility-props.d.ts +11 -0
- package/build/accessibility-props.js +30 -117
- package/build/behaviors/image.d.ts +3 -0
- package/build/behaviors/image.js +123 -0
- package/build/behaviors/input-accessory-view.d.ts +3 -0
- package/build/behaviors/input-accessory-view.js +70 -0
- package/build/behaviors/pressable.d.ts +2 -0
- package/build/behaviors/pressable.js +310 -0
- package/build/behaviors/switch.d.ts +2 -0
- package/build/behaviors/switch.js +182 -0
- package/build/behaviors/text-input.d.ts +14 -0
- package/build/behaviors/text-input.js +291 -0
- package/build/bootstrap/index.js +1 -1
- package/build/component-names/index.android.js +23 -1
- package/build/component-names/index.ios.js +9 -1
- package/build/component-names/shared.d.ts +1 -1
- package/build/component-names/shared.js +40 -1
- package/build/descriptor.d.ts +8 -0
- package/build/descriptor.js +27 -0
- package/build/fold-host-bag.d.ts +15 -0
- package/build/fold-host-bag.js +99 -0
- package/build/index.d.ts +25 -12
- package/build/index.js +18 -6
- package/build/resolve-intrinsic.d.ts +7 -0
- package/build/resolve-intrinsic.js +49 -0
- package/build/state/pressable.d.ts +9 -0
- package/build/state/pressable.js +133 -37
- package/build/state/sticky-header-reducer.js +28 -5
- package/build/state/switch.js +1 -1
- package/build/state/text-input.d.ts +7 -1
- package/build/state/text-input.js +46 -8
- package/build/state/touchable.d.ts +30 -1
- package/build/state/touchable.js +94 -5
- package/build/state/virtualized-list-diagnostics.d.ts +31 -0
- package/build/state/virtualized-list-diagnostics.js +33 -0
- package/build/state/virtualized-list-reducer.d.ts +14 -0
- package/build/state/virtualized-list-reducer.js +179 -45
- package/build/state/virtualized-list.d.ts +5 -2
- package/build/state/virtualized-list.js +143 -34
- package/build/state-style.d.ts +15 -0
- package/build/state-style.js +47 -0
- package/build/text-props.d.ts +9 -0
- package/build/text-props.js +25 -0
- package/build/view/render-activity-indicator.js +37 -3
- package/build/view/render-image/index.d.ts +2 -0
- package/build/view/render-image/index.js +42 -5
- package/build/view/render-input-accessory-view.d.ts +2 -0
- package/build/view/render-input-accessory-view.js +41 -6
- package/build/view/render-keyboard-avoiding-view.d.ts +14 -2
- package/build/view/render-keyboard-avoiding-view.js +52 -6
- package/build/view/render-modal.js +4 -2
- package/build/view/render-pressable/index.js +3 -1
- package/build/view/render-scroll-sticky.js +1 -1
- package/build/view/render-scroll-view.js +9 -3
- package/build/view/render-switch.js +11 -2
- package/build/view/render-text-input.js +6 -2
- package/build/view/render-touchable-highlight.d.ts +11 -1
- package/build/view/render-touchable-highlight.js +11 -10
- package/build/view/render-touchable-native-feedback.js +5 -1
- package/host-primitives.cjs +380 -0
- package/host-primitives.d.cts +35 -0
- package/lowering-fixtures.cjs +259 -0
- package/lowering-fixtures.d.cts +17 -0
- package/package.json +42 -5
- package/specialize-state-style.cjs +219 -0
- package/specialize-state-style.d.cts +15 -0
|
@@ -73,8 +73,9 @@ export interface IVirtualizedListHandle extends IScrollRoutingHandle {
|
|
|
73
73
|
}): void;
|
|
74
74
|
}
|
|
75
75
|
export declare function readScrollOffset(event: ISymbioteEvent, horizontal: boolean): number | undefined;
|
|
76
|
+
export declare function readLayoutOffset(event: ISymbioteEvent, horizontal: boolean): number | undefined;
|
|
76
77
|
export declare function readLayoutLength(event: ISymbioteEvent, horizontal: boolean): number | undefined;
|
|
77
|
-
export declare function buildOffsets(count: number, measured: Map<number, number>, fixedLayout: ((index: number) => ICellLayout) | undefined, averageLength: number): {
|
|
78
|
+
export declare function buildOffsets(count: number, measured: Map<number, number>, measuredOffsets: Map<number, number>, fixedLayout: ((index: number) => ICellLayout) | undefined, averageLength: number, averageStride: number): {
|
|
78
79
|
offsets: number[];
|
|
79
80
|
lengths: number[];
|
|
80
81
|
total: number;
|
|
@@ -96,7 +97,10 @@ export declare function throttleWindow(target: {
|
|
|
96
97
|
export declare function visiblePercent(cellOffset: number, cellLength: number, scrollOffset: number, viewportLength: number): number;
|
|
97
98
|
export declare function isCellViewable(percent: number, config: IViewabilityConfig): boolean;
|
|
98
99
|
export declare function offsetForIndex(index: number, viewPosition: number, viewOffset: number, count: number, offsets: number[], lengths: number[], viewportLength: number): number;
|
|
100
|
+
export declare const LAYOUT_EPSILON = 0.01;
|
|
101
|
+
export declare function isSettledLayout(known: number | undefined, reported: number): boolean;
|
|
99
102
|
export declare function averageMeasuredLength(measured: Map<number, number>): number;
|
|
103
|
+
export declare function averageMeasuredStride(measuredOffsets: Map<number, number>, fallback: number): number;
|
|
100
104
|
export declare function highestMeasuredIndex(measured: Map<number, number>): number;
|
|
101
105
|
export declare function computeEndReached(total: number, scrollOffset: number, viewportLength: number, thresholdMultiplier: number): {
|
|
102
106
|
distanceFromEnd: number;
|
|
@@ -152,7 +156,6 @@ export interface IListPlanParams {
|
|
|
152
156
|
keyFor: (index: number) => string;
|
|
153
157
|
stickyIndices?: ReadonlySet<number>;
|
|
154
158
|
hasHeader: boolean;
|
|
155
|
-
hasSeparators: boolean;
|
|
156
159
|
}
|
|
157
160
|
export declare function buildListPlan(params: IListPlanParams): IListPlan;
|
|
158
161
|
export type IMvcpAction = {
|
|
@@ -65,6 +65,19 @@ export function readScrollOffset(event, horizontal) {
|
|
|
65
65
|
return undefined;
|
|
66
66
|
return readNumber(offset, horizontal ? 'x' : 'y');
|
|
67
67
|
}
|
|
68
|
+
// The cell's own position inside the scroll content, as the host reported it. Paired with
|
|
69
|
+
// readLayoutLength at the same onLayout: this is the value buildOffsets stores VERBATIM, and it is
|
|
70
|
+
// what lets the table describe where the content actually is rather than where a sum of heights
|
|
71
|
+
// says it should be.
|
|
72
|
+
export function readLayoutOffset(event, horizontal) {
|
|
73
|
+
const native = asRecord(event.nativeEvent);
|
|
74
|
+
if (native === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
const layout = asRecord(native.layout);
|
|
77
|
+
if (layout === undefined)
|
|
78
|
+
return undefined;
|
|
79
|
+
return readNumber(layout, horizontal ? 'x' : 'y');
|
|
80
|
+
}
|
|
68
81
|
export function readLayoutLength(event, horizontal) {
|
|
69
82
|
const native = asRecord(event.nativeEvent);
|
|
70
83
|
if (native === undefined)
|
|
@@ -77,23 +90,53 @@ export function readLayoutLength(event, horizontal) {
|
|
|
77
90
|
// Resolve every cell offset/length from the cache (or getItemLayout), filling gaps with
|
|
78
91
|
// the running average so an unmeasured tail still has a plausible total. Returns the
|
|
79
92
|
// per-index offset table plus the grand total extent.
|
|
80
|
-
|
|
93
|
+
//
|
|
94
|
+
// THE COORDINATE SPACE IS THE HOST'S, NOT A MODEL'S. `measuredOffsets` holds each cell's raw y (x
|
|
95
|
+
// when horizontal) exactly as onLayout reported it — content-container relative, so it carries the
|
|
96
|
+
// container's padding, the list header, and whatever spacer stood above the cell at the time. A
|
|
97
|
+
// measured cell is placed at that value VERBATIM. It is never re-derived from a neighbour, never
|
|
98
|
+
// rebased onto a running sum. This is react-native's ListMetricsAggregator: `getCellMetricsApprox`
|
|
99
|
+
// returns a laid-out cell's real frame untouched and approximates only what has never been seen.
|
|
100
|
+
//
|
|
101
|
+
// That "verbatim" is the whole safety property, and it is not a stylistic preference — it is the
|
|
102
|
+
// fix for the canary going blank mid-scroll (diagnosed on device 2026-08-19). The table is not
|
|
103
|
+
// merely an output: buildListPlan sizes the leading spacer from it, the host lays the window's
|
|
104
|
+
// cells out below that spacer, and their measured y — spacer included — comes straight back in
|
|
105
|
+
// here. It is a closed loop. Combining two measurements arithmetically inside that loop feeds the
|
|
106
|
+
// model's own error back to itself: with a Yoga `gap` on the content container (the canary's .grid
|
|
107
|
+
// has one) a spacer is an extra flex child, so its presence shifts the layout by one more gap than
|
|
108
|
+
// any pure-arithmetic model predicts, and differencing two cells measured either side of that
|
|
109
|
+
// change banks the difference. Measured, at one gap per recompute, unbounded — onScroll fires per
|
|
110
|
+
// frame, so the spacer walks thousands of pixels away from reality within a second of dragging and
|
|
111
|
+
// the window lands nowhere near the viewport. Regression test:
|
|
112
|
+
// core/components/src/state/virtualized-list-feedback.test.ts.
|
|
113
|
+
//
|
|
114
|
+
// Unmeasured cells are the only place an estimate lives, and they advance by the average STRIDE
|
|
115
|
+
// (measured cell-origin to cell-origin) rather than by the average LENGTH. A height is not the
|
|
116
|
+
// distance to the next cell: separators, section gaps and container `gap` all live in between, and
|
|
117
|
+
// sizing an unmeasured region by heights alone leaves it short by exactly that chrome.
|
|
118
|
+
//
|
|
119
|
+
// A fixed getItemLayout skips all of it — authoritative by contract, and its offsets are exact.
|
|
120
|
+
export function buildOffsets(count, measured, measuredOffsets, fixedLayout, averageLength, averageStride) {
|
|
81
121
|
const offsets = new Array(count);
|
|
82
122
|
const lengths = new Array(count);
|
|
83
|
-
|
|
123
|
+
// What the average stride has left over once the average cell is accounted for: the chrome drawn
|
|
124
|
+
// BETWEEN two cells. Estimating with this rather than the stride itself keeps a cell whose own
|
|
125
|
+
// length IS known from being overwritten by an average — the stride is only ever used for the
|
|
126
|
+
// part nobody measured.
|
|
127
|
+
const interCellChrome = Math.max(EMPTY_OFFSET, averageStride - averageLength);
|
|
128
|
+
// Where the next cell goes when its own position has never been reported.
|
|
129
|
+
let cursor = EMPTY_OFFSET;
|
|
84
130
|
for (let index = FIRST_INDEX; index < count; index += 1) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
else {
|
|
91
|
-
length = measured.get(index) ?? averageLength;
|
|
92
|
-
}
|
|
93
|
-
lengths[index] = length;
|
|
94
|
-
running += length;
|
|
131
|
+
const layout = fixedLayout?.(index);
|
|
132
|
+
const known = layout?.offset ?? measuredOffsets.get(index);
|
|
133
|
+
offsets[index] = known ?? cursor;
|
|
134
|
+
lengths[index] = layout?.length ?? measured.get(index) ?? averageLength;
|
|
135
|
+
cursor = offsets[index] + lengths[index] + interCellChrome;
|
|
95
136
|
}
|
|
96
|
-
|
|
137
|
+
const last = count - 1;
|
|
138
|
+
const total = count > FIRST_INDEX ? offsets[last] + lengths[last] : EMPTY_OFFSET;
|
|
139
|
+
return { offsets, lengths, total };
|
|
97
140
|
}
|
|
98
141
|
// Pick the resident window: every index whose box overlaps
|
|
99
142
|
// [offset - buffer, offset + viewport + buffer]. The buffer is (windowSize - 1) / 2
|
|
@@ -103,7 +146,10 @@ export function computeWindow(count, offsets, lengths, scrollOffset, viewportLen
|
|
|
103
146
|
return { first: FIRST_INDEX, last: NO_INDEX };
|
|
104
147
|
// Before the viewport is known, paint a bounded prefix.
|
|
105
148
|
if (viewportLength <= EMPTY_OFFSET) {
|
|
106
|
-
return {
|
|
149
|
+
return {
|
|
150
|
+
first: FIRST_INDEX,
|
|
151
|
+
last: Math.min(count, initialNumToRender) - 1,
|
|
152
|
+
};
|
|
107
153
|
}
|
|
108
154
|
const overscan = ((windowSize - 1) / 2) * viewportLength;
|
|
109
155
|
const windowTop = scrollOffset - overscan;
|
|
@@ -149,7 +195,8 @@ export function isCellViewable(percent, config) {
|
|
|
149
195
|
const itemThreshold = config.itemVisiblePercentThreshold;
|
|
150
196
|
if (itemThreshold !== undefined)
|
|
151
197
|
return percent >= itemThreshold;
|
|
152
|
-
const areaThreshold = config.viewAreaCoveragePercentThreshold ??
|
|
198
|
+
const areaThreshold = config.viewAreaCoveragePercentThreshold ??
|
|
199
|
+
DEFAULT_VIEW_AREA_COVERAGE_PERCENT_THRESHOLD;
|
|
153
200
|
return percent > areaThreshold || percent >= FULLY_VISIBLE_PERCENT;
|
|
154
201
|
}
|
|
155
202
|
// Resolve an index to a pixel offset, optionally biasing where in the viewport the item
|
|
@@ -162,6 +209,24 @@ export function offsetForIndex(index, viewPosition, viewOffset, count, offsets,
|
|
|
162
209
|
const positioned = cellOffset - viewPosition * (viewportLength - cellLength);
|
|
163
210
|
return Math.max(EMPTY_OFFSET, positioned - viewOffset);
|
|
164
211
|
}
|
|
212
|
+
// Two layout readings are the SAME measurement unless they differ by more than this.
|
|
213
|
+
//
|
|
214
|
+
// A relayout does not reproduce a float bit-for-bit: the cell positions are derived from a spacer
|
|
215
|
+
// height that is itself a float, so an onLayout that changed nothing observable still comes back a
|
|
216
|
+
// few ulps off. Compared with ===, every one of those counts as a change — the reducer stores it,
|
|
217
|
+
// the spacer derived from it moves in its last bits, Fabric commits the new value, Yoga relays out,
|
|
218
|
+
// and the fresh onLayout starts the next turn. A loop at frame rate, from a difference no screen
|
|
219
|
+
// can show. Device-measured 2026-08-19: 1203 recomputes over one short drag, its log full of
|
|
220
|
+
// `27.33 -> 27.33 (-0.00)`.
|
|
221
|
+
//
|
|
222
|
+
// The smallest change a host can actually express is one device pixel — a third of a point at @3x —
|
|
223
|
+
// so this sits ~30x below any real move and ~1e11 above the noise.
|
|
224
|
+
export const LAYOUT_EPSILON = 0.01;
|
|
225
|
+
// `known` is optional because a first measurement has nothing to settle against, and must count as
|
|
226
|
+
// a change.
|
|
227
|
+
export function isSettledLayout(known, reported) {
|
|
228
|
+
return known !== undefined && Math.abs(known - reported) < LAYOUT_EPSILON;
|
|
229
|
+
}
|
|
165
230
|
// Running average of known cell lengths, used to size not-yet-measured cells and the
|
|
166
231
|
// trailing spacer so the total is plausible before full measurement.
|
|
167
232
|
export function averageMeasuredLength(measured) {
|
|
@@ -172,6 +237,27 @@ export function averageMeasuredLength(measured) {
|
|
|
172
237
|
sum += length;
|
|
173
238
|
return sum / measured.size;
|
|
174
239
|
}
|
|
240
|
+
// Average origin-to-origin distance between two ADJACENT measured cells — the length plus whatever
|
|
241
|
+
// chrome the list draws in the gap (a separator, a section gap, the content container's Yoga
|
|
242
|
+
// `gap`). Only adjacent pairs qualify: across a hole the distance covers cells nobody measured.
|
|
243
|
+
//
|
|
244
|
+
// This is what an unmeasured cell advances by, and it is deliberately not averageMeasuredLength.
|
|
245
|
+
// Sizing an unmeasured region by heights alone makes the model shorter than the content, so the
|
|
246
|
+
// spacer standing in for that region under-reserves and everything below it slides up — the
|
|
247
|
+
// jump-and-return the canary showed before the offsets became host-absolute. Falls back to the
|
|
248
|
+
// length average while no adjacent pair has been measured yet.
|
|
249
|
+
export function averageMeasuredStride(measuredOffsets, fallback) {
|
|
250
|
+
let sum = EMPTY_OFFSET;
|
|
251
|
+
let pairs = EMPTY_OFFSET;
|
|
252
|
+
for (const [index, offset] of measuredOffsets) {
|
|
253
|
+
const next = measuredOffsets.get(index + 1);
|
|
254
|
+
if (next === undefined)
|
|
255
|
+
continue;
|
|
256
|
+
sum += next - offset;
|
|
257
|
+
pairs += 1;
|
|
258
|
+
}
|
|
259
|
+
return pairs === EMPTY_OFFSET ? fallback : sum / pairs;
|
|
260
|
+
}
|
|
175
261
|
// The largest index whose length has actually been measured (RN
|
|
176
262
|
// ListMetricsAggregator.getHighestMeasuredCellIndex). NO_INDEX when nothing is measured.
|
|
177
263
|
export function highestMeasuredIndex(measured) {
|
|
@@ -204,7 +290,10 @@ export function computeStartReached(scrollOffset, viewportLength, thresholdMulti
|
|
|
204
290
|
export function buildViewabilityPairs(onViewableItemsChanged, viewabilityConfig, pairs) {
|
|
205
291
|
const result = [];
|
|
206
292
|
if (onViewableItemsChanged !== undefined) {
|
|
207
|
-
result.push({
|
|
293
|
+
result.push({
|
|
294
|
+
viewabilityConfig: viewabilityConfig ?? {},
|
|
295
|
+
onViewableItemsChanged,
|
|
296
|
+
});
|
|
208
297
|
}
|
|
209
298
|
if (pairs !== undefined)
|
|
210
299
|
result.push(...pairs);
|
|
@@ -219,10 +308,13 @@ export function computeViewableSet(params) {
|
|
|
219
308
|
for (let index = params.first; index <= params.last && index < params.count; index += 1) {
|
|
220
309
|
const percent = visiblePercent(params.offsets[index], params.lengths[index], params.scrollOffset, params.viewportLength);
|
|
221
310
|
const item = params.getItem(params.data, index);
|
|
222
|
-
const key = params.keyExtractor
|
|
311
|
+
const key = params.keyExtractor
|
|
312
|
+
? params.keyExtractor(item, index)
|
|
313
|
+
: String(index);
|
|
223
314
|
let anyViewable = false;
|
|
224
315
|
for (const pair of params.pairs) {
|
|
225
|
-
if (pair.viewabilityConfig.waitForInteraction === true &&
|
|
316
|
+
if (pair.viewabilityConfig.waitForInteraction === true &&
|
|
317
|
+
!params.hasInteracted) {
|
|
226
318
|
continue;
|
|
227
319
|
}
|
|
228
320
|
if (isCellViewable(percent, pair.viewabilityConfig)) {
|
|
@@ -299,22 +391,33 @@ export function buildListPlan(params) {
|
|
|
299
391
|
const forcedStickyCell = closestStickyIndex === NO_INDEX
|
|
300
392
|
? undefined
|
|
301
393
|
: { index: closestStickyIndex, key: params.keyFor(closestStickyIndex) };
|
|
302
|
-
|
|
303
|
-
|
|
394
|
+
// A spacer stands in for a contiguous run of cells, so its extent is the distance from the first
|
|
395
|
+
// of them to the far edge of the last — a difference between two positions the host itself
|
|
396
|
+
// reported, never a sum of heights. That is what carries the chrome BETWEEN those cells
|
|
397
|
+
// (separators, section gaps, the container's Yoga `gap`) without the model having to know it
|
|
398
|
+
// exists, and it is why the spacer lands the following cell exactly where it already was: the
|
|
399
|
+
// spacer occupies one child slot, precisely as the region it replaces began and ended on a cell
|
|
400
|
+
// boundary. Summing heights instead under-reserves by the chrome; rebasing onto a running model
|
|
401
|
+
// re-introduces the feedback loop buildOffsets exists to avoid.
|
|
402
|
+
const regionExtent = (from, to) => to < from
|
|
403
|
+
? EMPTY_OFFSET
|
|
404
|
+
: params.offsets[to] + params.lengths[to] - params.offsets[from];
|
|
405
|
+
const windowLeadingExtent = regionExtent(FIRST_INDEX, params.first - 1);
|
|
406
|
+
const leadingExtent = forcedStickyCell === undefined
|
|
407
|
+
? windowLeadingExtent
|
|
408
|
+
: regionExtent(FIRST_INDEX, closestStickyIndex - 1);
|
|
304
409
|
const gapExtent = forcedStickyCell === undefined
|
|
305
410
|
? EMPTY_OFFSET
|
|
306
|
-
:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const renderedExtent = params.last >= params.first
|
|
310
|
-
? params.offsets[params.last] + params.lengths[params.last] - params.offsets[params.first]
|
|
411
|
+
: regionExtent(closestStickyIndex + 1, params.first - 1);
|
|
412
|
+
const trailingExtent = params.last < params.count - 1
|
|
413
|
+
? params.total - params.offsets[params.last + 1]
|
|
311
414
|
: EMPTY_OFFSET;
|
|
312
|
-
const trailingExtent = params.total - windowLeadingExtent - renderedExtent;
|
|
313
415
|
const stickyChildPositions = [];
|
|
314
416
|
// The header (when present) is child 0; the leading spacer (when non-empty) is the next
|
|
315
|
-
// child; the forced sticky cell (when present) plus its own gap spacer follow. Each cell
|
|
316
|
-
//
|
|
317
|
-
//
|
|
417
|
+
// child; the forced sticky cell (when present) plus its own gap spacer follow. Each cell is
|
|
418
|
+
// EXACTLY one child — an ItemSeparatorComponent rides INSIDE the cell's own measuring wrapper
|
|
419
|
+
// (RN VirtualizedListCellRenderer.js:218-221), so it neither shifts these positions nor shows
|
|
420
|
+
// up in the geometry as chrome the spacers would have to account for separately.
|
|
318
421
|
let childPosition = (params.hasHeader ? 1 : 0) + (leadingExtent > EMPTY_OFFSET ? 1 : 0);
|
|
319
422
|
if (forcedStickyCell !== undefined) {
|
|
320
423
|
stickyChildPositions.push(childPosition);
|
|
@@ -325,8 +428,6 @@ export function buildListPlan(params) {
|
|
|
325
428
|
if (params.stickyIndices?.has(index) === true)
|
|
326
429
|
stickyChildPositions.push(childPosition);
|
|
327
430
|
childPosition += 1;
|
|
328
|
-
if (params.hasSeparators && index < params.last)
|
|
329
|
-
childPosition += 1;
|
|
330
431
|
}
|
|
331
432
|
return {
|
|
332
433
|
leadingExtent,
|
|
@@ -348,7 +449,9 @@ export function computeMvcpAdjustment(params) {
|
|
|
348
449
|
firstVisibleKey: newFirstVisibleKey,
|
|
349
450
|
action: { kind: 'none' },
|
|
350
451
|
};
|
|
351
|
-
if (prevKey === null ||
|
|
452
|
+
if (prevKey === null ||
|
|
453
|
+
newFirstVisibleKey === null ||
|
|
454
|
+
prevKey === newFirstVisibleKey) {
|
|
352
455
|
return settled;
|
|
353
456
|
}
|
|
354
457
|
let anchorIndex = NO_INDEX;
|
|
@@ -373,7 +476,10 @@ export function computeMvcpAdjustment(params) {
|
|
|
373
476
|
const anchoredNearTop = autoThreshold !== undefined && params.scrollOffset <= autoThreshold;
|
|
374
477
|
if (anchoredNearTop) {
|
|
375
478
|
dlog(`VirtualizedList MVCP autoscroll-to-top (offset=${params.scrollOffset} <= ${autoThreshold})`);
|
|
376
|
-
return {
|
|
479
|
+
return {
|
|
480
|
+
firstVisibleKey: newFirstVisibleKey,
|
|
481
|
+
action: { kind: 'autoscroll-top' },
|
|
482
|
+
};
|
|
377
483
|
}
|
|
378
484
|
dlog(`VirtualizedList MVCP adjust +${insertedExtent}px ` +
|
|
379
485
|
`(anchor "${prevKey}" moved ${minIndexForVisible}->${anchorIndex})`);
|
|
@@ -417,7 +523,10 @@ export function decideEdgeReached(params) {
|
|
|
417
523
|
}
|
|
418
524
|
// Re-arm once out of threshold so the next approach can fire again.
|
|
419
525
|
if (!withinThreshold) {
|
|
420
|
-
return {
|
|
526
|
+
return {
|
|
527
|
+
shouldFire: false,
|
|
528
|
+
nextSentForContentLength: NO_CONTENT_LENGTH_SENT,
|
|
529
|
+
};
|
|
421
530
|
}
|
|
422
531
|
return { shouldFire: false, nextSentForContentLength: sentForContentLength };
|
|
423
532
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface IPressStateArgument {
|
|
2
|
+
pressed: boolean;
|
|
3
|
+
}
|
|
4
|
+
export interface IResolvedStateStyle {
|
|
5
|
+
style: unknown;
|
|
6
|
+
activeStyle: unknown;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Splits an authored `style` into its resting and pressed halves, reading the value once.
|
|
10
|
+
*
|
|
11
|
+
* A non-callback passes through untouched with no active variant, which the engine reads as "leave
|
|
12
|
+
* slot 1 alone" — that is what makes one emission safe for an expression whose value cannot be
|
|
13
|
+
* known at compile time.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveStateStyle(value: unknown): IResolvedStateStyle;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The runtime half of state-style lowering: splits an authored `style` into its resting and pressed
|
|
2
|
+
// halves. Referenced by the code every lowering transform EMITS, never by app code.
|
|
3
|
+
//
|
|
4
|
+
// SHARED for the same reason `HOST_PRIMITIVES` and `specializeStateStyle` are — two adapters had
|
|
5
|
+
// written byte-identical copies within an hour of each other, which is the duplication
|
|
6
|
+
// `<adapters_stay_thin>` exists to stop. Each adapter re-exports it from its own `./state-style`
|
|
7
|
+
// subpath so the emitted import specifier stays inside the package the app already depends on.
|
|
8
|
+
//
|
|
9
|
+
// WHY A HELPER EXISTS BESIDE THE INLINE GUARD, since a transform may emit either. Building the pair
|
|
10
|
+
// needs the authored `style` expression, and the two emissions are NOT equivalent:
|
|
11
|
+
//
|
|
12
|
+
// inline typeof e === 'function' ? e({pressed:false}) : e prints `e` on both props, so a
|
|
13
|
+
// `getStyle()` runs twice and a `flag ? a : b` can take DIFFERENT branches
|
|
14
|
+
// helper resolveStateStyle(e) prints `e` once and calls its
|
|
15
|
+
// RESULT twice
|
|
16
|
+
//
|
|
17
|
+
// The rule the two must satisfy is `REFUSAL_CATEGORIES.emitStyleExpressionOnce`, and it is about
|
|
18
|
+
// the output rather than the input: **an expression capable of DOING WORK is printed once.** A bare
|
|
19
|
+
// name or a non-computed dotted path is a read, not work, so the inline form is legitimate there —
|
|
20
|
+
// and it matters, because a helper returns one object, which a framework must spread, and a spread
|
|
21
|
+
// costs the element its patch flag (Vue measured `12 /* STYLE, PROPS */` becoming
|
|
22
|
+
// `16 /* FULL_PROPS */` plus a `mergeProps` per render on the hottest element in the tree).
|
|
23
|
+
//
|
|
24
|
+
// So the VERDICT is identical across adapters — every shape lowers — and only the cost differs,
|
|
25
|
+
// exactly as compile-time substitution differs between a JSX path and an SFC one. The verdicts are
|
|
26
|
+
// what `@symbiote-native/components/lowering-fixtures` pins.
|
|
27
|
+
//
|
|
28
|
+
// THE CONTRACT THIS IMPOSES ON APP CODE: a style callback must be PURE in `pressed`. Its result is
|
|
29
|
+
// invoked twice under every emission, so a side-effecting body is observable now.
|
|
30
|
+
function isStyleCallback(value) {
|
|
31
|
+
return typeof value === 'function';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Splits an authored `style` into its resting and pressed halves, reading the value once.
|
|
35
|
+
*
|
|
36
|
+
* A non-callback passes through untouched with no active variant, which the engine reads as "leave
|
|
37
|
+
* slot 1 alone" — that is what makes one emission safe for an expression whose value cannot be
|
|
38
|
+
* known at compile time.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveStateStyle(value) {
|
|
41
|
+
if (!isStyleCallback(value))
|
|
42
|
+
return { style: value, activeStyle: undefined };
|
|
43
|
+
return {
|
|
44
|
+
style: value({ pressed: false }),
|
|
45
|
+
activeStyle: value({ pressed: true }),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type IEllipsizeMode = 'head' | 'middle' | 'tail' | 'clip';
|
|
2
|
+
export interface ITextDefaultableProps {
|
|
3
|
+
ellipsizeMode?: IEllipsizeMode;
|
|
4
|
+
allowFontScaling?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveTextProps<T extends ITextDefaultableProps>(props: T): T & {
|
|
7
|
+
ellipsizeMode: IEllipsizeMode;
|
|
8
|
+
allowFontScaling: boolean;
|
|
9
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The defaults RN's Text.js applies before handing props to native, folded once for every adapter.
|
|
2
|
+
//
|
|
3
|
+
// RN does this unconditionally on the outer (non-virtual) path, Text.js:289 and :291 (the two
|
|
4
|
+
// lines between them, 288 and 290, are `accessible` and `disabled` — not part of this fold):
|
|
5
|
+
//
|
|
6
|
+
// processedProps.allowFontScaling = allowFontScaling !== false;
|
|
7
|
+
// processedProps.ellipsizeMode = ellipsizeMode ?? 'tail';
|
|
8
|
+
//
|
|
9
|
+
// We declared both props in all four adapters and applied NEITHER default, so native fell back to
|
|
10
|
+
// its own: `clip` instead of `tail`. Device-observed 2026-08-19 on examples/svelte — a Text with
|
|
11
|
+
// numberOfLines={1} cut mid-word with no ellipsis at all. Nothing failed; the text was simply
|
|
12
|
+
// wrong, which is why four adapters carried it.
|
|
13
|
+
//
|
|
14
|
+
// Both keys are emitted UNCONDITIONALLY, matching RN and — separately — keeping the key set
|
|
15
|
+
// stable. A fold with two branches that emit different key sets is the exact hazard
|
|
16
|
+
// .claude/rules/solid-descriptor-bridge.md §1 exists for, and Svelte's shim diffs the same way.
|
|
17
|
+
export function resolveTextProps(props) {
|
|
18
|
+
return {
|
|
19
|
+
...props,
|
|
20
|
+
ellipsizeMode: props.ellipsizeMode ?? 'tail',
|
|
21
|
+
// `!== false`, not `?? true`: RN treats an explicit `undefined` and a missing prop alike, and
|
|
22
|
+
// only a literal `false` opts out.
|
|
23
|
+
allowFontScaling: props.allowFontScaling !== false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
// ACTIVITYINDICATOR IS NOT A LOWERABLE PRIMITIVE, AND WILL NOT BECOME ONE. Decided 2026-09-01.
|
|
2
|
+
// It stays a component in every adapter; do not add it to `HOST_PRIMITIVES`.
|
|
3
|
+
//
|
|
4
|
+
// The reason is the `el('symbiote-view', wrapperProps, [el('symbiote-activity-indicator', …)])` at
|
|
5
|
+
// the bottom of this file: the render SYNTHESIZES a node that is not the primitive itself. A lowered
|
|
6
|
+
// tag is ONE engine node, and a host behavior's `foldPayload` maps props to props on that node — it
|
|
7
|
+
// cannot create a child. Lowering would therefore drop the centering container and change layout,
|
|
8
|
+
// which is an optimisation moving the observable surface, the one thing this design may not do.
|
|
9
|
+
//
|
|
10
|
+
// The wrapper is not over-building: RN's own `ActivityIndicator.js:112` renders a `<View>` around
|
|
11
|
+
// the native spinner too, so the two nodes are inherent. And the container cannot be folded INTO the
|
|
12
|
+
// spinner — it carries `alignItems`/`justifyContent`, which centre the spinner inside the space it
|
|
13
|
+
// was given; moved onto the spinner they would centre its children, of which it has none.
|
|
14
|
+
//
|
|
15
|
+
// The two alternatives were priced and both cost more than the primitive is worth. A behavior that
|
|
16
|
+
// CREATES a node needs a commit hook, i.e. a machine, i.e. a `-managed` tag split — a new category,
|
|
17
|
+
// not a fold. Synthesising the container inside the engine's commit walk is NOT the `RCTVirtualText`
|
|
18
|
+
// precedent it resembles: `viewNameFor` changes what one node IS and never ADDS one, so adding one
|
|
19
|
+
// would make the retained and committed trees disagree on node count — which every counter, census
|
|
20
|
+
// probe and benchmark in this repo assumes. Spinners are rare on a screen, so the per-instance cost
|
|
21
|
+
// lowering removes is not measurably paid here; the objection to leaving it is uniformity, not speed.
|
|
22
|
+
//
|
|
23
|
+
// Under half A this costs an app NOTHING: `View` and `ActivityIndicator` are both ordinary imports
|
|
24
|
+
// from the adapter barrel, so which one is internally a tag and which a component is invisible at
|
|
25
|
+
// the call site. The general rule is in `.claude/rules/host-primitive-tier.md`.
|
|
26
|
+
//
|
|
1
27
|
// ActivityIndicator: the render half (framework-agnostic). RN wraps the native spinner
|
|
2
28
|
// in a centering View and translates `size` in JS: 'small'/'large' map to a native size
|
|
3
29
|
// enum AND a fixed box style; a numeric size never reaches native (it sizes the spinner
|
|
@@ -20,10 +46,16 @@ const CONTAINER_STYLE = {
|
|
|
20
46
|
};
|
|
21
47
|
function resolveSize(size) {
|
|
22
48
|
if (size === 'small') {
|
|
23
|
-
return {
|
|
49
|
+
return {
|
|
50
|
+
sizeStyle: { width: SIZE_SMALL_PX, height: SIZE_SMALL_PX },
|
|
51
|
+
sizeProp: 'small',
|
|
52
|
+
};
|
|
24
53
|
}
|
|
25
54
|
if (size === 'large') {
|
|
26
|
-
return {
|
|
55
|
+
return {
|
|
56
|
+
sizeStyle: { width: SIZE_LARGE_PX, height: SIZE_LARGE_PX },
|
|
57
|
+
sizeProp: 'large',
|
|
58
|
+
};
|
|
27
59
|
}
|
|
28
60
|
return { sizeStyle: { width: size, height: size } };
|
|
29
61
|
}
|
|
@@ -50,5 +82,7 @@ export function renderActivityIndicator(view, platform) {
|
|
|
50
82
|
...view.passthrough,
|
|
51
83
|
style: [CONTAINER_STYLE, view.style],
|
|
52
84
|
};
|
|
53
|
-
return el('symbiote-view', wrapperProps, [
|
|
85
|
+
return el('symbiote-view', wrapperProps, [
|
|
86
|
+
el('symbiote-activity-indicator', nativeProps),
|
|
87
|
+
]);
|
|
54
88
|
}
|
|
@@ -53,4 +53,6 @@ export type IImageViewProps = {
|
|
|
53
53
|
referrerPolicy?: string;
|
|
54
54
|
passthrough: Record<string, unknown>;
|
|
55
55
|
};
|
|
56
|
+
export declare const IMAGE_VIEW_PROP_NAMES: readonly ["source", "defaultSource", "loadingIndicatorSource", "style", "resizeMode", "tintColor", "src", "srcSet", "alt", "width", "height", "crossOrigin", "referrerPolicy"];
|
|
57
|
+
export declare function mapImageProps(view: IImageViewProps): Record<string, unknown>;
|
|
56
58
|
export declare function renderImage(view: IImageViewProps): IDescriptor;
|
|
@@ -39,7 +39,13 @@ function expandSrcSet(srcSet, view, headers) {
|
|
|
39
39
|
continue;
|
|
40
40
|
if (scale === 1)
|
|
41
41
|
useSrcForDefaultScale = false;
|
|
42
|
-
sources.push({
|
|
42
|
+
sources.push({
|
|
43
|
+
uri,
|
|
44
|
+
scale,
|
|
45
|
+
width: view.width,
|
|
46
|
+
height: view.height,
|
|
47
|
+
...{ headers },
|
|
48
|
+
});
|
|
43
49
|
}
|
|
44
50
|
if (useSrcForDefaultScale && view.src !== undefined) {
|
|
45
51
|
sources.push({
|
|
@@ -65,7 +71,9 @@ function resolveSourceArray(view) {
|
|
|
65
71
|
return expandSrcSet(view.srcSet, view, headers);
|
|
66
72
|
}
|
|
67
73
|
if (view.src !== undefined) {
|
|
68
|
-
return [
|
|
74
|
+
return [
|
|
75
|
+
{ uri: view.src, width: view.width, height: view.height, ...{ headers } },
|
|
76
|
+
];
|
|
69
77
|
}
|
|
70
78
|
if (view.source === undefined) {
|
|
71
79
|
dlog('Image: no source / src / srcSet provided');
|
|
@@ -76,7 +84,9 @@ function resolveSourceArray(view) {
|
|
|
76
84
|
// `source.uri && headers` branch; the array/number shapes pass through untouched.
|
|
77
85
|
if (Object.keys(headers).length > 0 && sources.length === 1) {
|
|
78
86
|
const [only] = sources;
|
|
79
|
-
if (typeof only === 'object' &&
|
|
87
|
+
if (typeof only === 'object' &&
|
|
88
|
+
only !== null &&
|
|
89
|
+
typeof Reflect.get(only, 'uri') === 'string') {
|
|
80
90
|
return [{ ...only, headers }];
|
|
81
91
|
}
|
|
82
92
|
}
|
|
@@ -102,7 +112,31 @@ function readSourceUri(source) {
|
|
|
102
112
|
}
|
|
103
113
|
return undefined;
|
|
104
114
|
}
|
|
105
|
-
|
|
115
|
+
// The names `renderImage` CONSUMES rather than forwards — the fold's input list, and the reason it
|
|
116
|
+
// is exported: an adapter that splits props before calling renderImage was copying this list by
|
|
117
|
+
// hand, and a name added here reached the copy only if someone remembered. The Image behavior's
|
|
118
|
+
// `foldPayload` reads it too, so the flat path and the view path cannot drift apart.
|
|
119
|
+
export const IMAGE_VIEW_PROP_NAMES = [
|
|
120
|
+
'source',
|
|
121
|
+
'defaultSource',
|
|
122
|
+
'loadingIndicatorSource',
|
|
123
|
+
'style',
|
|
124
|
+
'resizeMode',
|
|
125
|
+
'tintColor',
|
|
126
|
+
'src',
|
|
127
|
+
'srcSet',
|
|
128
|
+
'alt',
|
|
129
|
+
'width',
|
|
130
|
+
'height',
|
|
131
|
+
'crossOrigin',
|
|
132
|
+
'referrerPolicy',
|
|
133
|
+
];
|
|
134
|
+
// The whole of Image's prop mapping, with the `el()` wrap taken off. Split out because a LOWERED
|
|
135
|
+
// `<symbiote-image>` has no component body to run it in: its behavior's `foldPayload` needs the same
|
|
136
|
+
// bag at commit time, and a second implementation of this is the exact drift this file already paid
|
|
137
|
+
// for once (`adapters/svelte/src/components/image/image-logic.ts` reproduces it by hand, with a
|
|
138
|
+
// comment saying it had to because nothing here was exported).
|
|
139
|
+
export function mapImageProps(view) {
|
|
106
140
|
// `width` / `height` aliases fold into style (ImageProps.js:195,202); explicit
|
|
107
141
|
// style keys win, matching RN's `{width, height}, ...style` ordering.
|
|
108
142
|
const foldedStyle = view.width === undefined && view.height === undefined
|
|
@@ -128,6 +162,9 @@ export function renderImage(view) {
|
|
|
128
162
|
if (view.loadingIndicatorSource !== undefined) {
|
|
129
163
|
mapped.loadingIndicatorSrc = readSourceUri(view.loadingIndicatorSource);
|
|
130
164
|
}
|
|
165
|
+
return mapped;
|
|
166
|
+
}
|
|
167
|
+
export function renderImage(view) {
|
|
131
168
|
dlog('Image -> RCTImageView');
|
|
132
|
-
return el('symbiote-image',
|
|
169
|
+
return el('symbiote-image', mapImageProps(view));
|
|
133
170
|
}
|
|
@@ -6,4 +6,6 @@ export type IInputAccessoryViewViewProps = {
|
|
|
6
6
|
style?: IStyleProp<IViewStyle>;
|
|
7
7
|
passthrough: Record<string, unknown>;
|
|
8
8
|
};
|
|
9
|
+
export declare const INPUT_ACCESSORY_VIEW_PROP_NAMES: readonly ["nativeID", "backgroundColor", "style"];
|
|
10
|
+
export declare function mapInputAccessoryViewProps(view: IInputAccessoryViewViewProps): Record<string, unknown>;
|
|
9
11
|
export declare function renderInputAccessoryView(view: IInputAccessoryViewViewProps): IDescriptor;
|
|
@@ -4,15 +4,50 @@
|
|
|
4
4
|
// the two by id. There is no JS-side translation: style / nativeID / backgroundColor map straight
|
|
5
5
|
// onto the intrinsic and the user children (injected by the adapter) nest under it. Shared
|
|
6
6
|
// verbatim across adapters: React and Vue both bridge this Descriptor.
|
|
7
|
-
import { dlog } from '@symbiote-native/engine';
|
|
7
|
+
import { dlog, } from '@symbiote-native/engine';
|
|
8
8
|
import { el } from '../descriptor.js';
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// The names this fold CONSUMES, exported so an adapter splitting props before calling the render fn
|
|
10
|
+
// reads the list instead of copying it — the drift `render-image.ts` records paying for once.
|
|
11
|
+
export const INPUT_ACCESSORY_VIEW_PROP_NAMES = [
|
|
12
|
+
'nativeID',
|
|
13
|
+
'backgroundColor',
|
|
14
|
+
'style',
|
|
15
|
+
];
|
|
16
|
+
// The whole mapping, so the wrapper path and the lowered path share ONE implementation. There is no
|
|
17
|
+
// aliasing here at all: every consumed name leaves under the same name, which is what makes the
|
|
18
|
+
// fold idempotent by construction (asserted in `behaviors/input-accessory-view.test.ts`, not
|
|
19
|
+
// assumed).
|
|
20
|
+
//
|
|
21
|
+
// The `undefined` guards on `nativeID` / `backgroundColor` are LOAD-BEARING, and the asymmetry with
|
|
22
|
+
// the unguarded `style` above is not the tell it looks like.
|
|
23
|
+
//
|
|
24
|
+
// They were removed on 2026-09-01 on the reasoning `.claude/rules/fabric-boolean-event-gates.md`
|
|
25
|
+
// states — `setProp` collapses an undefined value to an absent key, so a conditional write in a
|
|
26
|
+
// shared render fn is cosmetic — and that reasoning was checked against a payload carrying no `id`,
|
|
27
|
+
// which is exactly the case where it holds. The equivalence oracle caught it on its first run:
|
|
28
|
+
//
|
|
29
|
+
// authored <InputAccessoryView id="p" testID="p" />
|
|
30
|
+
// guarded RCTInputAccessoryView{testID, nativeID:"p"}
|
|
31
|
+
// unguarded RCTInputAccessoryView{testID} <- the alias result, deleted
|
|
32
|
+
//
|
|
33
|
+
// `nativeID` has an ALIAS SOURCE. The wrapper leaves `id` in `passthrough`, the renderer's
|
|
34
|
+
// PROP_ALIASES renames it to `nativeID`, and a `nativeID: undefined` written afterwards deletes what
|
|
35
|
+
// the rename just produced — last write wins, and `undefined` collapsing to "absent" is precisely
|
|
36
|
+
// what makes it destructive rather than inert. So the guard is cosmetic for a key nothing else can
|
|
37
|
+
// produce, and required for a key an alias also targets.
|
|
38
|
+
export function mapInputAccessoryViewProps(view) {
|
|
39
|
+
const out = {
|
|
40
|
+
...view.passthrough,
|
|
41
|
+
style: view.style,
|
|
42
|
+
};
|
|
11
43
|
if (view.nativeID !== undefined)
|
|
12
|
-
|
|
44
|
+
out.nativeID = view.nativeID;
|
|
13
45
|
if (view.backgroundColor !== undefined)
|
|
14
|
-
|
|
46
|
+
out.backgroundColor = view.backgroundColor;
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
export function renderInputAccessoryView(view) {
|
|
15
50
|
dlog('InputAccessoryView -> RCTInputAccessoryView');
|
|
16
51
|
// Empty structural children: the adapter appends the user children directly under the host.
|
|
17
|
-
return el('symbiote-input-accessory-view',
|
|
52
|
+
return el('symbiote-input-accessory-view', mapInputAccessoryViewProps(view), []);
|
|
18
53
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IStyleProp, IViewStyle } from '@symbiote-native/engine';
|
|
1
|
+
import type { IKeyboardEventName, IPlatformOSType, IStyleProp, IViewStyle } from '@symbiote-native/engine';
|
|
2
2
|
export type IKeyboardAvoidingBehavior = 'height' | 'position' | 'padding';
|
|
3
3
|
export declare const DEFAULT_VERTICAL_OFFSET = 0;
|
|
4
4
|
export interface IMeasuredFrame {
|
|
@@ -11,7 +11,19 @@ export interface IKeyboardFrame {
|
|
|
11
11
|
}
|
|
12
12
|
export declare function readKeyboardFrame(payload: unknown): IKeyboardFrame | undefined;
|
|
13
13
|
export declare function readLayoutFrame(layout: unknown): IMeasuredFrame | undefined;
|
|
14
|
-
export
|
|
14
|
+
export type IKeyboardAvoidingEventNames = {
|
|
15
|
+
show: IKeyboardEventName;
|
|
16
|
+
hide: IKeyboardEventName;
|
|
17
|
+
};
|
|
18
|
+
export declare function keyboardAvoidingEventNamesFor(os: IPlatformOSType): IKeyboardAvoidingEventNames;
|
|
19
|
+
export declare function readPrefersCrossFadeTransitions(query?: () => Promise<boolean>): Promise<boolean>;
|
|
20
|
+
export type IComputeInsetOptions = {
|
|
21
|
+
behavior?: IKeyboardAvoidingBehavior;
|
|
22
|
+
previousInset?: number;
|
|
23
|
+
os?: IPlatformOSType;
|
|
24
|
+
prefersCrossFadeTransitions?: boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare function computeInset(frame: IMeasuredFrame | undefined, keyboard: IKeyboardFrame | undefined, verticalOffset: number, options?: IComputeInsetOptions): number;
|
|
15
27
|
export type IKeyboardAvoidingLayout = {
|
|
16
28
|
kind: 'nested';
|
|
17
29
|
wrapperStyle?: IStyleProp<IViewStyle>;
|