@symbiote-native/components 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/build/accessibility-props.d.ts +64 -0
- package/build/accessibility-props.js +150 -0
- package/build/component-names/index.android.d.ts +3 -0
- package/build/component-names/index.android.js +32 -0
- package/build/component-names/index.d.ts +1 -0
- package/build/component-names/index.ios.d.ts +3 -0
- package/build/component-names/index.ios.js +26 -0
- package/build/component-names/index.js +5 -0
- package/build/component-names/shared.d.ts +7 -0
- package/build/component-names/shared.js +36 -0
- package/build/descriptor.d.ts +11 -0
- package/build/descriptor.js +17 -0
- package/build/index.d.ts +51 -0
- package/build/index.js +63 -0
- package/build/responder-props.d.ts +18 -0
- package/build/responder-props.js +9 -0
- package/build/scroll-view-commands.d.ts +23 -0
- package/build/scroll-view-commands.js +123 -0
- package/build/state/drawer-layout-android.d.ts +22 -0
- package/build/state/drawer-layout-android.js +53 -0
- package/build/state/flat-list.d.ts +12 -0
- package/build/state/flat-list.js +40 -0
- package/build/state/modal.d.ts +11 -0
- package/build/state/modal.js +28 -0
- package/build/state/pressable.d.ts +90 -0
- package/build/state/pressable.js +236 -0
- package/build/state/section-list.d.ts +46 -0
- package/build/state/section-list.js +51 -0
- package/build/state/switch.d.ts +12 -0
- package/build/state/switch.js +31 -0
- package/build/state/text-input.d.ts +103 -0
- package/build/state/text-input.js +205 -0
- package/build/state/touchable.d.ts +15 -0
- package/build/state/touchable.js +22 -0
- package/build/state/virtualized-list.d.ts +161 -0
- package/build/state/virtualized-list.js +306 -0
- package/build/view/render-activity-indicator.d.ts +25 -0
- package/build/view/render-activity-indicator.js +54 -0
- package/build/view/render-button.d.ts +19 -0
- package/build/view/render-button.js +24 -0
- package/build/view/render-drawer-layout-android.d.ts +23 -0
- package/build/view/render-drawer-layout-android.js +56 -0
- package/build/view/render-image-background.d.ts +9 -0
- package/build/view/render-image-background.js +48 -0
- package/build/view/render-image.d.ts +86 -0
- package/build/view/render-image.js +298 -0
- package/build/view/render-input-accessory-view.d.ts +9 -0
- package/build/view/render-input-accessory-view.js +18 -0
- package/build/view/render-keyboard-avoiding-view.d.ts +30 -0
- package/build/view/render-keyboard-avoiding-view.js +75 -0
- package/build/view/render-modal.d.ts +23 -0
- package/build/view/render-modal.js +70 -0
- package/build/view/render-pressable.d.ts +8 -0
- package/build/view/render-pressable.js +42 -0
- package/build/view/render-scroll-sticky.d.ts +24 -0
- package/build/view/render-scroll-sticky.js +81 -0
- package/build/view/render-scroll-view.d.ts +18 -0
- package/build/view/render-scroll-view.js +85 -0
- package/build/view/render-switch.d.ts +29 -0
- package/build/view/render-switch.js +33 -0
- package/build/view/render-text-input.d.ts +11 -0
- package/build/view/render-text-input.js +35 -0
- package/build/view/render-touchable-native-feedback.d.ts +17 -0
- package/build/view/render-touchable-native-feedback.js +39 -0
- package/package.json +38 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// VirtualizedList logic: the framework-agnostic windowing engine. Every adapter
|
|
2
|
+
// (React hooks, Vue reactivity) drives the SAME math from here, so a windowing /
|
|
3
|
+
// viewability / edge-reached bug is fixed once for all adapters
|
|
4
|
+
// (<adapters_reach_full_feature_parity>). The adapter supplies only its lifecycle
|
|
5
|
+
// (refs/state/effects), the imperative handle wiring, and the per-cell element
|
|
6
|
+
// creation (createElement / h) — never the geometry.
|
|
7
|
+
//
|
|
8
|
+
// What lives here:
|
|
9
|
+
// - the RN-matching defaults + sentinels,
|
|
10
|
+
// - the nativeEvent payload readers (scroll offset / layout length),
|
|
11
|
+
// - offset table + window computation + batch throttling,
|
|
12
|
+
// - viewability classification, the viewable-set diff, and the minimumViewTime fold,
|
|
13
|
+
// - the edge-reached (onEndReached / onStartReached) distance + threshold compute,
|
|
14
|
+
// - the assembled child PLAN (spacer extents, in-window cell keys, sticky child
|
|
15
|
+
// positions) the adapter maps onto its host elements,
|
|
16
|
+
// - the shared data + imperative-handle types.
|
|
17
|
+
//
|
|
18
|
+
// What stays in the adapter (genuinely framework-bound): the cell CONTENT is the
|
|
19
|
+
// framework's own children (renderItem -> ReactNode / VNode), so there is no
|
|
20
|
+
// Descriptor render fn for a list — the shared layer for lists is this STATE/logic
|
|
21
|
+
// module, not a view/render-*.ts. See core/components/.docs-note-lists.md.
|
|
22
|
+
// Defaults match RN. windowSize is measured in viewport-lengths (21 => ten screens
|
|
23
|
+
// of buffer on each side of the visible region). onEndReachedThreshold is a multiple
|
|
24
|
+
// of the visible length (RN's onEndReachedThresholdOrDefault returns `?? 2`).
|
|
25
|
+
// initialNumToRender bounds the first paint before any layout is measured.
|
|
26
|
+
// maxToRenderPerBatch / batching period mirror RN's incremental fill defaults.
|
|
27
|
+
export const DEFAULT_WINDOW_SIZE = 21;
|
|
28
|
+
export const DEFAULT_INITIAL_NUM_TO_RENDER = 10;
|
|
29
|
+
export const DEFAULT_END_REACHED_THRESHOLD = 2;
|
|
30
|
+
export const DEFAULT_MAX_TO_RENDER_PER_BATCH = 10;
|
|
31
|
+
export const DEFAULT_UPDATE_CELLS_BATCHING_PERIOD = 50;
|
|
32
|
+
export const DEFAULT_VIEW_AREA_COVERAGE_PERCENT_THRESHOLD = 0;
|
|
33
|
+
// onStartReachedThreshold default, mirroring RN's onStartReachedThresholdOrDefault.
|
|
34
|
+
export const DEFAULT_START_REACHED_THRESHOLD = 2;
|
|
35
|
+
export const FIRST_INDEX = 0;
|
|
36
|
+
export const EMPTY_OFFSET = 0;
|
|
37
|
+
export const NO_INDEX = -1;
|
|
38
|
+
export const FULLY_VISIBLE_PERCENT = 100;
|
|
39
|
+
// RN floors sub-pixel end distances to 0 so a debounced scroll that stops a fraction
|
|
40
|
+
// of a pixel from the bottom still counts as "reached the end" (RN VirtualizedList.js).
|
|
41
|
+
export const ON_EDGE_REACHED_EPSILON = 0.001;
|
|
42
|
+
// Sentinel for "onEndReached / onStartReached has not fired for any content length
|
|
43
|
+
// yet". Real content lengths are >= 0, so -1 can never collide with one.
|
|
44
|
+
export const NO_CONTENT_LENGTH_SENT = -1;
|
|
45
|
+
// Inversion flips the content container along the scroll axis; each cell re-flips so
|
|
46
|
+
// its own content stays upright (RN does the same with a scale(-1) transform).
|
|
47
|
+
export const INVERTED_Y_STYLE = { transform: [{ scaleY: -1 }] };
|
|
48
|
+
export const INVERTED_X_STYLE = { transform: [{ scaleX: -1 }] };
|
|
49
|
+
// nativeEvent payload guards. The payloads arrive as `unknown` off the wire, so they
|
|
50
|
+
// are narrowed with runtime checks rather than cast.
|
|
51
|
+
function readNumber(source, key) {
|
|
52
|
+
const value = source[key];
|
|
53
|
+
return typeof value === 'number' ? value : undefined;
|
|
54
|
+
}
|
|
55
|
+
function asRecord(value) {
|
|
56
|
+
return typeof value === 'object' && value !== null ? { ...value } : undefined;
|
|
57
|
+
}
|
|
58
|
+
// onScroll -> the offset along the scroll axis. Vertical reads contentOffset.y,
|
|
59
|
+
// horizontal reads contentOffset.x.
|
|
60
|
+
export function readScrollOffset(event, horizontal) {
|
|
61
|
+
const native = asRecord(event.nativeEvent);
|
|
62
|
+
if (native === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
const offset = asRecord(native.contentOffset);
|
|
65
|
+
if (offset === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
return readNumber(offset, horizontal ? 'x' : 'y');
|
|
68
|
+
}
|
|
69
|
+
// onLayout -> the cross-section length of the box along the scroll axis.
|
|
70
|
+
export function readLayoutLength(event, horizontal) {
|
|
71
|
+
const native = asRecord(event.nativeEvent);
|
|
72
|
+
if (native === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
const layout = asRecord(native.layout);
|
|
75
|
+
if (layout === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
return readNumber(layout, horizontal ? 'width' : 'height');
|
|
78
|
+
}
|
|
79
|
+
// Resolve every cell offset/length from the cache (or getItemLayout), filling gaps with
|
|
80
|
+
// the running average so an unmeasured tail still has a plausible total. Returns the
|
|
81
|
+
// per-index offset table plus the grand total extent.
|
|
82
|
+
export function buildOffsets(count, measured, fixedLayout, averageLength) {
|
|
83
|
+
const offsets = new Array(count);
|
|
84
|
+
const lengths = new Array(count);
|
|
85
|
+
let running = EMPTY_OFFSET;
|
|
86
|
+
for (let index = FIRST_INDEX; index < count; index += 1) {
|
|
87
|
+
offsets[index] = running;
|
|
88
|
+
let length;
|
|
89
|
+
if (fixedLayout !== undefined) {
|
|
90
|
+
length = fixedLayout(index).length;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
length = measured.get(index) ?? averageLength;
|
|
94
|
+
}
|
|
95
|
+
lengths[index] = length;
|
|
96
|
+
running += length;
|
|
97
|
+
}
|
|
98
|
+
return { offsets, lengths, total: running };
|
|
99
|
+
}
|
|
100
|
+
// Pick the resident window: every index whose box overlaps
|
|
101
|
+
// [offset - buffer, offset + viewport + buffer]. The buffer is (windowSize - 1) / 2
|
|
102
|
+
// viewport-lengths on each side, matching RN's symmetric leading/trailing overscan.
|
|
103
|
+
export function computeWindow(count, offsets, lengths, scrollOffset, viewportLength, windowSize, initialNumToRender) {
|
|
104
|
+
if (count === FIRST_INDEX)
|
|
105
|
+
return { first: FIRST_INDEX, last: NO_INDEX };
|
|
106
|
+
// Before the viewport is known, paint a bounded prefix.
|
|
107
|
+
if (viewportLength <= EMPTY_OFFSET) {
|
|
108
|
+
return { first: FIRST_INDEX, last: Math.min(count, initialNumToRender) - 1 };
|
|
109
|
+
}
|
|
110
|
+
const overscan = ((windowSize - 1) / 2) * viewportLength;
|
|
111
|
+
const windowTop = scrollOffset - overscan;
|
|
112
|
+
const windowBottom = scrollOffset + viewportLength + overscan;
|
|
113
|
+
let first = FIRST_INDEX;
|
|
114
|
+
while (first < count - 1 && offsets[first] + lengths[first] <= windowTop) {
|
|
115
|
+
first += 1;
|
|
116
|
+
}
|
|
117
|
+
let last = first;
|
|
118
|
+
while (last < count - 1 && offsets[last] + lengths[last] < windowBottom) {
|
|
119
|
+
last += 1;
|
|
120
|
+
}
|
|
121
|
+
return { first, last };
|
|
122
|
+
}
|
|
123
|
+
// Clamp a freshly computed window against the previously-committed one so at most
|
|
124
|
+
// maxToRenderPerBatch new cells are added on each side per tick (RN's incremental fill).
|
|
125
|
+
// The window grows toward the target over successive batch ticks rather than snapping in
|
|
126
|
+
// one render: cheaper first paint on a big jump.
|
|
127
|
+
export function throttleWindow(target, previous, maxToRenderPerBatch) {
|
|
128
|
+
if (previous.last < previous.first)
|
|
129
|
+
return target;
|
|
130
|
+
const first = Math.max(target.first, previous.first - maxToRenderPerBatch);
|
|
131
|
+
const last = Math.min(target.last, previous.last + maxToRenderPerBatch);
|
|
132
|
+
// Never present an empty window when the target is non-empty.
|
|
133
|
+
if (last < first)
|
|
134
|
+
return target;
|
|
135
|
+
return { first, last };
|
|
136
|
+
}
|
|
137
|
+
// Fraction (0..100) of a cell's box that lies inside the viewport.
|
|
138
|
+
export function visiblePercent(cellOffset, cellLength, scrollOffset, viewportLength) {
|
|
139
|
+
if (cellLength <= EMPTY_OFFSET)
|
|
140
|
+
return EMPTY_OFFSET;
|
|
141
|
+
const top = Math.max(cellOffset, scrollOffset);
|
|
142
|
+
const bottom = Math.min(cellOffset + cellLength, scrollOffset + viewportLength);
|
|
143
|
+
const visible = Math.max(EMPTY_OFFSET, bottom - top);
|
|
144
|
+
return (visible / cellLength) * FULLY_VISIBLE_PERCENT;
|
|
145
|
+
}
|
|
146
|
+
// A cell is viewable when its visible fraction clears the configured threshold.
|
|
147
|
+
// itemVisiblePercentThreshold compares against the cell's own size;
|
|
148
|
+
// viewAreaCoveragePercentThreshold compares against the viewport. The former wins when
|
|
149
|
+
// set, else the latter, matching RN's precedence.
|
|
150
|
+
export function isCellViewable(percent, config) {
|
|
151
|
+
const itemThreshold = config.itemVisiblePercentThreshold;
|
|
152
|
+
if (itemThreshold !== undefined)
|
|
153
|
+
return percent >= itemThreshold;
|
|
154
|
+
const areaThreshold = config.viewAreaCoveragePercentThreshold ?? DEFAULT_VIEW_AREA_COVERAGE_PERCENT_THRESHOLD;
|
|
155
|
+
return percent > areaThreshold || percent >= FULLY_VISIBLE_PERCENT;
|
|
156
|
+
}
|
|
157
|
+
// Resolve an index to a pixel offset, optionally biasing where in the viewport the item
|
|
158
|
+
// lands (viewPosition 0=top, 1=bottom, 0.5=center) and an absolute viewOffset nudge,
|
|
159
|
+
// mirroring RN's scrollToIndex options.
|
|
160
|
+
export function offsetForIndex(index, viewPosition, viewOffset, count, offsets, lengths, viewportLength) {
|
|
161
|
+
const clamped = Math.max(FIRST_INDEX, Math.min(index, count - 1));
|
|
162
|
+
const cellOffset = offsets[clamped] ?? EMPTY_OFFSET;
|
|
163
|
+
const cellLength = lengths[clamped] ?? EMPTY_OFFSET;
|
|
164
|
+
const positioned = cellOffset - viewPosition * (viewportLength - cellLength);
|
|
165
|
+
return Math.max(EMPTY_OFFSET, positioned - viewOffset);
|
|
166
|
+
}
|
|
167
|
+
// Running average of known cell lengths, used to size not-yet-measured cells and the
|
|
168
|
+
// trailing spacer so the total is plausible before full measurement.
|
|
169
|
+
export function averageMeasuredLength(measured) {
|
|
170
|
+
if (measured.size === EMPTY_OFFSET)
|
|
171
|
+
return EMPTY_OFFSET;
|
|
172
|
+
let sum = EMPTY_OFFSET;
|
|
173
|
+
for (const length of measured.values())
|
|
174
|
+
sum += length;
|
|
175
|
+
return sum / measured.size;
|
|
176
|
+
}
|
|
177
|
+
// The largest index whose length has actually been measured (RN
|
|
178
|
+
// ListMetricsAggregator.getHighestMeasuredCellIndex). NO_INDEX when nothing is measured.
|
|
179
|
+
export function highestMeasuredIndex(measured) {
|
|
180
|
+
let highest = NO_INDEX;
|
|
181
|
+
for (const index of measured.keys()) {
|
|
182
|
+
if (index > highest)
|
|
183
|
+
highest = index;
|
|
184
|
+
}
|
|
185
|
+
return highest;
|
|
186
|
+
}
|
|
187
|
+
// onEndReached distance + threshold test (RN _maybeCallOnEdgeReached). The adapter still
|
|
188
|
+
// gates on "the last cell is actually rendered" and dedups by content length via its own
|
|
189
|
+
// ref; this returns only the pure geometry.
|
|
190
|
+
export function computeEndReached(total, scrollOffset, viewportLength, thresholdMultiplier) {
|
|
191
|
+
let distanceFromEnd = total - (scrollOffset + viewportLength);
|
|
192
|
+
if (distanceFromEnd < ON_EDGE_REACHED_EPSILON)
|
|
193
|
+
distanceFromEnd = EMPTY_OFFSET;
|
|
194
|
+
const threshold = thresholdMultiplier * viewportLength;
|
|
195
|
+
return { distanceFromEnd, withinThreshold: distanceFromEnd <= threshold };
|
|
196
|
+
}
|
|
197
|
+
// onStartReached twin of computeEndReached. distanceFromStart is just the scroll offset.
|
|
198
|
+
export function computeStartReached(scrollOffset, viewportLength, thresholdMultiplier) {
|
|
199
|
+
let distanceFromStart = scrollOffset;
|
|
200
|
+
if (distanceFromStart < ON_EDGE_REACHED_EPSILON)
|
|
201
|
+
distanceFromStart = EMPTY_OFFSET;
|
|
202
|
+
const threshold = thresholdMultiplier * viewportLength;
|
|
203
|
+
return { distanceFromStart, withinThreshold: distanceFromStart <= threshold };
|
|
204
|
+
}
|
|
205
|
+
// Fold the single-config and pairs forms into one list (RN supports either, not both).
|
|
206
|
+
export function buildViewabilityPairs(onViewableItemsChanged, viewabilityConfig, pairs) {
|
|
207
|
+
const result = [];
|
|
208
|
+
if (onViewableItemsChanged !== undefined) {
|
|
209
|
+
result.push({ viewabilityConfig: viewabilityConfig ?? {}, onViewableItemsChanged });
|
|
210
|
+
}
|
|
211
|
+
if (pairs !== undefined)
|
|
212
|
+
result.push(...pairs);
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
// Classify every rendered cell against the viewability configs. A cell counts as
|
|
216
|
+
// viewable if ANY config says so (RN's broadest classification); a config with
|
|
217
|
+
// waitForInteraction classifies nothing until the first scroll has happened.
|
|
218
|
+
export function computeViewableSet(params) {
|
|
219
|
+
const tokens = [];
|
|
220
|
+
const map = new Map();
|
|
221
|
+
for (let index = params.first; index <= params.last && index < params.count; index += 1) {
|
|
222
|
+
const percent = visiblePercent(params.offsets[index], params.lengths[index], params.scrollOffset, params.viewportLength);
|
|
223
|
+
const item = params.getItem(params.data, index);
|
|
224
|
+
const key = params.keyExtractor ? params.keyExtractor(item, index) : String(index);
|
|
225
|
+
let anyViewable = false;
|
|
226
|
+
for (const pair of params.pairs) {
|
|
227
|
+
if (pair.viewabilityConfig.waitForInteraction === true && !params.hasInteracted) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (isCellViewable(percent, pair.viewabilityConfig)) {
|
|
231
|
+
anyViewable = true;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (anyViewable) {
|
|
236
|
+
const token = { item, key, index, isViewable: true };
|
|
237
|
+
map.set(key, token);
|
|
238
|
+
tokens.push(token);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { tokens, map };
|
|
242
|
+
}
|
|
243
|
+
// The `changed` delta between two viewable sets: newly viewable (true) and newly hidden
|
|
244
|
+
// (false). hasChanged is false when the viewable KEY set is identical, so the adapter can
|
|
245
|
+
// skip firing (RN dedups the same way). Hidden tokens come straight from the previous map,
|
|
246
|
+
// so no rescan of all N items.
|
|
247
|
+
export function diffViewable(previous, current, currentTokens) {
|
|
248
|
+
let hasChanged = previous.size !== current.size;
|
|
249
|
+
if (!hasChanged) {
|
|
250
|
+
for (const key of current.keys()) {
|
|
251
|
+
if (!previous.has(key)) {
|
|
252
|
+
hasChanged = true;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!hasChanged)
|
|
258
|
+
return { changed: [], hasChanged: false };
|
|
259
|
+
const changed = [];
|
|
260
|
+
for (const token of currentTokens) {
|
|
261
|
+
if (!previous.has(token.key))
|
|
262
|
+
changed.push(token);
|
|
263
|
+
}
|
|
264
|
+
for (const [key, token] of previous) {
|
|
265
|
+
if (!current.has(key))
|
|
266
|
+
changed.push({ ...token, isViewable: false });
|
|
267
|
+
}
|
|
268
|
+
return { changed, hasChanged: true };
|
|
269
|
+
}
|
|
270
|
+
// The largest configured minimumViewTime across all pairs (RN gates the unified pass on
|
|
271
|
+
// the largest value, since we fold all pairs into one classification).
|
|
272
|
+
export function maxMinimumViewTime(pairs) {
|
|
273
|
+
let max = EMPTY_OFFSET;
|
|
274
|
+
for (const pair of pairs) {
|
|
275
|
+
const configured = pair.viewabilityConfig.minimumViewTime;
|
|
276
|
+
if (configured !== undefined && configured > max)
|
|
277
|
+
max = configured;
|
|
278
|
+
}
|
|
279
|
+
return max;
|
|
280
|
+
}
|
|
281
|
+
// Compute the windowed child PLAN: the two spacer extents, the in-window cells (index +
|
|
282
|
+
// key), and the sticky child positions. The adapter walks this plan and creates the host
|
|
283
|
+
// elements (createElement / h) plus the framework cell content. This is the shared half of
|
|
284
|
+
// the render; only the element creation and the user's renderItem stay per-adapter.
|
|
285
|
+
export function buildListPlan(params) {
|
|
286
|
+
const cells = [];
|
|
287
|
+
const leadingExtent = params.first > FIRST_INDEX ? params.offsets[params.first] : EMPTY_OFFSET;
|
|
288
|
+
const renderedExtent = params.last >= params.first
|
|
289
|
+
? params.offsets[params.last] + params.lengths[params.last] - params.offsets[params.first]
|
|
290
|
+
: EMPTY_OFFSET;
|
|
291
|
+
const trailingExtent = params.total - leadingExtent - renderedExtent;
|
|
292
|
+
const stickyChildPositions = [];
|
|
293
|
+
// The header (when present) is child 0; the leading spacer (when non-empty) is the next
|
|
294
|
+
// child. Each cell is one child; a separator after it (when ItemSeparatorComponent is set
|
|
295
|
+
// and this is not the last cell) is another.
|
|
296
|
+
let childPosition = (params.hasHeader ? 1 : 0) + (leadingExtent > EMPTY_OFFSET ? 1 : 0);
|
|
297
|
+
for (let index = params.first; index <= params.last; index += 1) {
|
|
298
|
+
cells.push({ index, key: params.keyFor(index) });
|
|
299
|
+
if (params.stickyIndices?.has(index) === true)
|
|
300
|
+
stickyChildPositions.push(childPosition);
|
|
301
|
+
childPosition += 1;
|
|
302
|
+
if (params.hasSeparators && index < params.last)
|
|
303
|
+
childPosition += 1;
|
|
304
|
+
}
|
|
305
|
+
return { leadingExtent, trailingExtent, cells, stickyChildPositions };
|
|
306
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IStyleProp, IViewStyle, ISymbioteEvent } from '@symbiote-native/engine';
|
|
2
|
+
import type { IDescriptor } from '../descriptor';
|
|
3
|
+
import type { IAccessibilityProps, IAriaProps } from '../accessibility-props';
|
|
4
|
+
export type IActivityIndicatorSize = 'small' | 'large' | number;
|
|
5
|
+
export interface IActivityIndicatorProps extends IAccessibilityProps, IAriaProps {
|
|
6
|
+
animating?: boolean;
|
|
7
|
+
color?: string;
|
|
8
|
+
size?: IActivityIndicatorSize;
|
|
9
|
+
hidesWhenStopped?: boolean;
|
|
10
|
+
style?: IStyleProp<IViewStyle>;
|
|
11
|
+
onLayout?: (event: ISymbioteEvent) => void;
|
|
12
|
+
}
|
|
13
|
+
export type IActivityIndicatorViewProps = {
|
|
14
|
+
animating: boolean;
|
|
15
|
+
hidesWhenStopped: boolean;
|
|
16
|
+
size: IActivityIndicatorSize;
|
|
17
|
+
color?: string;
|
|
18
|
+
style?: IStyleProp<IViewStyle>;
|
|
19
|
+
passthrough: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
export type IActivityIndicatorPlatform = {
|
|
22
|
+
defaultColor: string | null;
|
|
23
|
+
nativeExtras: Readonly<Record<string, unknown>>;
|
|
24
|
+
};
|
|
25
|
+
export declare function renderActivityIndicator(view: IActivityIndicatorViewProps, platform: IActivityIndicatorPlatform): IDescriptor;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// ActivityIndicator: the render half (framework-agnostic). RN wraps the native spinner
|
|
2
|
+
// in a centering View and translates `size` in JS: 'small'/'large' map to a native size
|
|
3
|
+
// enum AND a fixed box style; a numeric size never reaches native (it sizes the spinner
|
|
4
|
+
// via style only). That translation is platform-invariant and lives here.
|
|
5
|
+
//
|
|
6
|
+
// What IS platform-specific (ADR 0020, prop-level): Android's AndroidProgressBar needs
|
|
7
|
+
// `styleAttr` (which triggers its setStyle(), without it the view throws "setStyle() not
|
|
8
|
+
// called") plus `indeterminate: true`, and its default color is the theme (null), whereas
|
|
9
|
+
// iOS's ActivityIndicatorView takes neither and defaults to GRAY. The adapter's per-host
|
|
10
|
+
// file supplies those bits via `platform`.
|
|
11
|
+
import { dlog } from '@symbiote-native/engine';
|
|
12
|
+
import { el } from '../descriptor';
|
|
13
|
+
// Fixed pixel boxes RN gives the two named sizes (styles.sizeSmall/sizeLarge).
|
|
14
|
+
const SIZE_SMALL_PX = 20;
|
|
15
|
+
const SIZE_LARGE_PX = 36;
|
|
16
|
+
// Centering wrapper RN puts around the spinner (styles.container).
|
|
17
|
+
const CONTAINER_STYLE = {
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
justifyContent: 'center',
|
|
20
|
+
};
|
|
21
|
+
function resolveSize(size) {
|
|
22
|
+
if (size === 'small') {
|
|
23
|
+
return { sizeStyle: { width: SIZE_SMALL_PX, height: SIZE_SMALL_PX }, sizeProp: 'small' };
|
|
24
|
+
}
|
|
25
|
+
if (size === 'large') {
|
|
26
|
+
return { sizeStyle: { width: SIZE_LARGE_PX, height: SIZE_LARGE_PX }, sizeProp: 'large' };
|
|
27
|
+
}
|
|
28
|
+
return { sizeStyle: { width: size, height: size } };
|
|
29
|
+
}
|
|
30
|
+
export function renderActivityIndicator(view, platform) {
|
|
31
|
+
const { sizeStyle, sizeProp } = resolveSize(view.size);
|
|
32
|
+
dlog(sizeProp !== undefined
|
|
33
|
+
? `ActivityIndicator size '${sizeProp}' -> native size enum '${sizeProp}'`
|
|
34
|
+
: `ActivityIndicator size ${String(view.size)} -> style only, native size not set`);
|
|
35
|
+
const nativeProps = {
|
|
36
|
+
animating: view.animating,
|
|
37
|
+
hidesWhenStopped: view.hidesWhenStopped,
|
|
38
|
+
style: sizeStyle,
|
|
39
|
+
...platform.nativeExtras,
|
|
40
|
+
};
|
|
41
|
+
// Omit color entirely when neither given nor defaulted (Android's theme default is
|
|
42
|
+
// null); a null color prop would be rejected by Fabric's color parser.
|
|
43
|
+
const resolvedColor = view.color ?? platform.defaultColor;
|
|
44
|
+
if (resolvedColor !== null)
|
|
45
|
+
nativeProps.color = resolvedColor;
|
|
46
|
+
if (sizeProp !== undefined)
|
|
47
|
+
nativeProps.size = sizeProp;
|
|
48
|
+
dlog('ActivityIndicator -> RCTView(spinner)');
|
|
49
|
+
const wrapperProps = {
|
|
50
|
+
...view.passthrough,
|
|
51
|
+
style: [CONTAINER_STYLE, view.style],
|
|
52
|
+
};
|
|
53
|
+
return el('symbiote-view', wrapperProps, [el('symbiote-activity-indicator', nativeProps)]);
|
|
54
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ITextStyle, ISymbioteEvent } from '@symbiote-native/engine';
|
|
2
|
+
import type { IAccessibilityProps, IAriaProps } from '../accessibility-props';
|
|
3
|
+
export interface IButtonProps extends IAccessibilityProps, IAriaProps {
|
|
4
|
+
title: string;
|
|
5
|
+
onPress?: (event: ISymbioteEvent) => void;
|
|
6
|
+
color?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
touchSoundDisabled?: boolean;
|
|
9
|
+
testID?: string;
|
|
10
|
+
hasTVPreferredFocus?: boolean;
|
|
11
|
+
nextFocusDown?: number;
|
|
12
|
+
nextFocusForward?: number;
|
|
13
|
+
nextFocusLeft?: number;
|
|
14
|
+
nextFocusRight?: number;
|
|
15
|
+
nextFocusUp?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const BUTTON_ACCESSIBILITY_ROLE = "button";
|
|
18
|
+
export declare const buttonTextStyle: ITextStyle;
|
|
19
|
+
export declare function resolveButtonTextStyle(color: string | undefined, disabled: boolean | undefined): ITextStyle;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Button: the shared render half (framework-agnostic). Rendered in RN's iOS shape (Button.js): a
|
|
2
|
+
// TouchableOpacity wrapping a Text. The pure pieces: the base text style, the role constant, and
|
|
3
|
+
// the color fold (caller color tints the label; disabled greys it) live here so every adapter
|
|
4
|
+
// paints the identical button. The adapter only composes its TouchableOpacity + Text around them.
|
|
5
|
+
const IOS_BUTTON_BLUE = '#007AFF';
|
|
6
|
+
const IOS_DISABLED_GREY = '#cdcdcd';
|
|
7
|
+
// RN's Button is accessibilityRole="button"; the role string is a native accessibility enum value.
|
|
8
|
+
export const BUTTON_ACCESSIBILITY_ROLE = 'button';
|
|
9
|
+
export const buttonTextStyle = {
|
|
10
|
+
color: IOS_BUTTON_BLUE,
|
|
11
|
+
textAlign: 'center',
|
|
12
|
+
padding: 8,
|
|
13
|
+
fontSize: 18,
|
|
14
|
+
};
|
|
15
|
+
// The label text style with the color folded in: an explicit `color` tints the label (iOS), and
|
|
16
|
+
// `disabled` greys it out (disabled wins over color, matching RN's Button.js).
|
|
17
|
+
export function resolveButtonTextStyle(color, disabled) {
|
|
18
|
+
const style = { ...buttonTextStyle };
|
|
19
|
+
if (color !== undefined)
|
|
20
|
+
style.color = color;
|
|
21
|
+
if (disabled === true)
|
|
22
|
+
style.color = IOS_DISABLED_GREY;
|
|
23
|
+
return style;
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IStyleProp, IViewStyle } from '@symbiote/engine';
|
|
2
|
+
import { type IDrawerLockMode, type IDrawerPosition, type IKeyboardDismissMode } from '../state/drawer-layout-android';
|
|
3
|
+
export declare const DRAWER_HOST_STYLE: Readonly<IViewStyle>;
|
|
4
|
+
export declare const DRAWER_MAIN_SUBVIEW_STYLE: Readonly<IViewStyle>;
|
|
5
|
+
export interface IDrawerLayoutResolveInput {
|
|
6
|
+
drawerWidth?: number;
|
|
7
|
+
drawerPosition?: IDrawerPosition;
|
|
8
|
+
drawerLockMode?: IDrawerLockMode;
|
|
9
|
+
keyboardDismissMode?: IKeyboardDismissMode;
|
|
10
|
+
drawerBackgroundColor?: string;
|
|
11
|
+
statusBarBackgroundColor?: string;
|
|
12
|
+
drawerOpened: boolean;
|
|
13
|
+
style?: IStyleProp<IViewStyle>;
|
|
14
|
+
passthrough: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface IDrawerLayoutResolved {
|
|
17
|
+
viewName: string;
|
|
18
|
+
hostProps: Record<string, unknown>;
|
|
19
|
+
contentWrapperStyle: Readonly<IViewStyle>;
|
|
20
|
+
navigationWrapperStyle: IViewStyle;
|
|
21
|
+
navigationPointerEvents: 'auto' | 'none';
|
|
22
|
+
}
|
|
23
|
+
export declare function resolveDrawerLayout(input: IDrawerLayoutResolveInput): IDrawerLayoutResolved;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// DrawerLayoutAndroid: the framework-agnostic view (style/prop) math. AndroidDrawerLayout is an
|
|
2
|
+
// ordinary Fabric host node committing through the same childSet as the rest of the tree (like
|
|
3
|
+
// Modal / Switch): a content wrapper FIRST and a navigation wrapper SECOND, mirroring RN's android
|
|
4
|
+
// render order {childrenWrapper}{drawerViewWrapper}. This builds the host prop bag + the two wrapper
|
|
5
|
+
// styles from the resolved props; the adapter overlays its ref + the wrapped event handlers and
|
|
6
|
+
// supplies the framework children / navigation elements. The logic half (event normalization,
|
|
7
|
+
// imperative handle, constants, types) lives in state/drawer-layout-android.ts.
|
|
8
|
+
import { DEFAULT_DRAWER_BACKGROUND_COLOR, DEFAULT_DRAWER_POSITION, DRAWER_VIEW_NAME, } from '../state/drawer-layout-android';
|
|
9
|
+
// RN styles.base on the host: flex:1 plus the Android drop-shadow that floats the drawer over
|
|
10
|
+
// content (android styles.base { flex:1, elevation:16 }).
|
|
11
|
+
export const DRAWER_HOST_STYLE = {
|
|
12
|
+
flex: 1,
|
|
13
|
+
elevation: 16,
|
|
14
|
+
};
|
|
15
|
+
// RN styles.mainSubview: the content wrapper fills the host (absolute, all edges 0).
|
|
16
|
+
export const DRAWER_MAIN_SUBVIEW_STYLE = {
|
|
17
|
+
position: 'absolute',
|
|
18
|
+
top: 0,
|
|
19
|
+
left: 0,
|
|
20
|
+
right: 0,
|
|
21
|
+
bottom: 0,
|
|
22
|
+
};
|
|
23
|
+
// RN styles.drawerSubview: the navigation wrapper is absolute and full-height; its width comes from
|
|
24
|
+
// drawerWidth and its background from drawerBackgroundColor (both folded in by resolveDrawerLayout).
|
|
25
|
+
const DRAWER_SUBVIEW_STYLE = {
|
|
26
|
+
position: 'absolute',
|
|
27
|
+
top: 0,
|
|
28
|
+
bottom: 0,
|
|
29
|
+
};
|
|
30
|
+
// Build the AndroidDrawerLayout host prop bag + the two wrapper styles from the resolved props. The
|
|
31
|
+
// adapter overlays its ref + the four wrapped event handlers (onDrawerOpen / Close / Slide /
|
|
32
|
+
// StateChanged) and nests [contentWrapper, navigationWrapper] under the host IN THAT ORDER.
|
|
33
|
+
export function resolveDrawerLayout(input) {
|
|
34
|
+
const drawerBackgroundColor = input.drawerBackgroundColor ?? DEFAULT_DRAWER_BACKGROUND_COLOR;
|
|
35
|
+
const hostProps = {
|
|
36
|
+
...input.passthrough,
|
|
37
|
+
drawerWidth: input.drawerWidth,
|
|
38
|
+
drawerPosition: input.drawerPosition ?? DEFAULT_DRAWER_POSITION,
|
|
39
|
+
drawerLockMode: input.drawerLockMode,
|
|
40
|
+
keyboardDismissMode: input.keyboardDismissMode,
|
|
41
|
+
drawerBackgroundColor,
|
|
42
|
+
statusBarBackgroundColor: input.statusBarBackgroundColor,
|
|
43
|
+
style: [DRAWER_HOST_STYLE, input.style],
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
viewName: DRAWER_VIEW_NAME,
|
|
47
|
+
hostProps,
|
|
48
|
+
contentWrapperStyle: DRAWER_MAIN_SUBVIEW_STYLE,
|
|
49
|
+
navigationWrapperStyle: {
|
|
50
|
+
...DRAWER_SUBVIEW_STYLE,
|
|
51
|
+
width: input.drawerWidth,
|
|
52
|
+
backgroundColor: drawerBackgroundColor,
|
|
53
|
+
},
|
|
54
|
+
navigationPointerEvents: input.drawerOpened ? 'auto' : 'none',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
|
|
2
|
+
import { type IDescriptor } from '../descriptor';
|
|
3
|
+
import { type IImageViewProps } from './render-image';
|
|
4
|
+
export type IImageBackgroundViewProps = {
|
|
5
|
+
style?: IStyleProp<IViewStyle>;
|
|
6
|
+
imageStyle?: IStyleProp<IViewStyle>;
|
|
7
|
+
image: IImageViewProps;
|
|
8
|
+
};
|
|
9
|
+
export declare function renderImageBackground(view: IImageBackgroundViewProps): IDescriptor;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// ImageBackground: the render half (framework-agnostic). Pure JS composition, no native
|
|
2
|
+
// component of its own (mirrors react-native/Libraries/Image/ImageBackground.js): an outer
|
|
3
|
+
// View receives the wrapper `style`; an absolutely-filled Image sits behind it; the user's
|
|
4
|
+
// `children` paint on top (as siblings AFTER the image in the wrapper's child order, injected
|
|
5
|
+
// by the adapter). Shared verbatim across adapters: React and Vue both bridge this Descriptor.
|
|
6
|
+
//
|
|
7
|
+
// The inner Image is positioned absolute-fill and has the wrapper's width/height reapplied:
|
|
8
|
+
// RN's Image overwrites its own width/height from the source's intrinsic size, which would
|
|
9
|
+
// fight the wrapper's explicit dimensions, so we proxy them back onto the Image so it fills
|
|
10
|
+
// the box. `imageStyle` wins last.
|
|
11
|
+
import { dlog, flattenStyle, } from '@symbiote-native/engine';
|
|
12
|
+
import { el } from '../descriptor';
|
|
13
|
+
import { renderImage } from './render-image';
|
|
14
|
+
// The inner Image's positioning: absolute-fill behind the wrapper's children.
|
|
15
|
+
const IMAGE_BACKGROUND_ABSOLUTE_FILL = {
|
|
16
|
+
position: 'absolute',
|
|
17
|
+
left: 0,
|
|
18
|
+
right: 0,
|
|
19
|
+
top: 0,
|
|
20
|
+
bottom: 0,
|
|
21
|
+
};
|
|
22
|
+
// Read one explicit dimension off the (already-flattened) wrapper style. A dp number or a
|
|
23
|
+
// percentage string is a valid IDimensionValue; anything else (auto / undefined) yields undefined.
|
|
24
|
+
function readDimension(style, key) {
|
|
25
|
+
const value = Object.hasOwn(style, key) ? Reflect.get(style, key) : undefined;
|
|
26
|
+
if (typeof value === 'number' || typeof value === 'string')
|
|
27
|
+
return value;
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
export function renderImageBackground(view) {
|
|
31
|
+
// Flatten only to read the wrapper's explicit dimensions; RN copies these onto the Image so
|
|
32
|
+
// it fills the box rather than collapsing to the source's intrinsic size. `imageStyle` last.
|
|
33
|
+
const flattenedWrapper = flattenStyle(view.style);
|
|
34
|
+
const imageMergedStyle = [
|
|
35
|
+
IMAGE_BACKGROUND_ABSOLUTE_FILL,
|
|
36
|
+
{
|
|
37
|
+
width: readDimension(flattenedWrapper, 'width'),
|
|
38
|
+
height: readDimension(flattenedWrapper, 'height'),
|
|
39
|
+
},
|
|
40
|
+
view.imageStyle,
|
|
41
|
+
];
|
|
42
|
+
dlog('ImageBackground -> View(RCTView) > Image(RCTImageView absolute-fill) + children');
|
|
43
|
+
// The wrapper View holds the inner Image as its only structural child; the adapter appends
|
|
44
|
+
// the user children after it (so they paint on top).
|
|
45
|
+
return el('symbiote-view', { style: view.style }, [
|
|
46
|
+
renderImage({ ...view.image, style: imageMergedStyle }),
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type IStyleProp, type ISymbioteEvent, type IViewStyle } from '@symbiote-native/engine';
|
|
2
|
+
import type { IAccessibilityProps, IAriaProps } from '../accessibility-props';
|
|
3
|
+
import { type IDescriptor } from '../descriptor';
|
|
4
|
+
type IImageEventHandler = (event: ISymbioteEvent) => void;
|
|
5
|
+
export type IResizeMode = 'cover' | 'contain' | 'stretch' | 'repeat' | 'center';
|
|
6
|
+
export type IImageSource = {
|
|
7
|
+
uri?: string;
|
|
8
|
+
scale?: number;
|
|
9
|
+
width?: number;
|
|
10
|
+
height?: number;
|
|
11
|
+
};
|
|
12
|
+
export type IImageSourceProp = IImageSource | IImageSource[] | number;
|
|
13
|
+
export type IImageCapInsets = {
|
|
14
|
+
top: number;
|
|
15
|
+
left: number;
|
|
16
|
+
bottom: number;
|
|
17
|
+
right: number;
|
|
18
|
+
};
|
|
19
|
+
export type IResizeMethod = 'auto' | 'resize' | 'scale' | 'none';
|
|
20
|
+
export type IImageProps = IAccessibilityProps & IAriaProps & {
|
|
21
|
+
source?: IImageSourceProp;
|
|
22
|
+
defaultSource?: IImageSourceProp;
|
|
23
|
+
loadingIndicatorSource?: IImageSourceProp;
|
|
24
|
+
style?: IStyleProp<IViewStyle>;
|
|
25
|
+
resizeMode?: IResizeMode;
|
|
26
|
+
resizeMethod?: IResizeMethod;
|
|
27
|
+
tintColor?: string;
|
|
28
|
+
blurRadius?: number;
|
|
29
|
+
capInsets?: IImageCapInsets;
|
|
30
|
+
fadeDuration?: number;
|
|
31
|
+
progressiveRenderingEnabled?: boolean;
|
|
32
|
+
src?: string;
|
|
33
|
+
srcSet?: string;
|
|
34
|
+
alt?: string;
|
|
35
|
+
width?: number;
|
|
36
|
+
height?: number;
|
|
37
|
+
crossOrigin?: 'anonymous' | 'use-credentials';
|
|
38
|
+
referrerPolicy?: string;
|
|
39
|
+
onLoadStart?: IImageEventHandler;
|
|
40
|
+
onLoad?: IImageEventHandler;
|
|
41
|
+
onLoadEnd?: IImageEventHandler;
|
|
42
|
+
onError?: IImageEventHandler;
|
|
43
|
+
onProgress?: IImageEventHandler;
|
|
44
|
+
onPartialLoad?: IImageEventHandler;
|
|
45
|
+
};
|
|
46
|
+
export declare function setImageSourceResolver(resolve: (source: unknown) => unknown): void;
|
|
47
|
+
export type IImageSize = {
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
};
|
|
51
|
+
export type IImageCacheStatus = 'memory' | 'disk' | 'disk/memory';
|
|
52
|
+
type ISizeSuccess = (width: number, height: number) => void;
|
|
53
|
+
type ISizeFailure = (error: unknown) => void;
|
|
54
|
+
declare function getSize(uri: string, success?: ISizeSuccess, failure?: ISizeFailure): Promise<IImageSize>;
|
|
55
|
+
declare function getSizeWithHeaders(uri: string, headers: Record<string, string>, success?: ISizeSuccess, failure?: ISizeFailure): Promise<IImageSize>;
|
|
56
|
+
declare function prefetch(uri: string, callback?: (requestId: number) => void): Promise<boolean>;
|
|
57
|
+
declare function abortPrefetch(requestId: number): void;
|
|
58
|
+
declare function queryCache(uris: string[]): Promise<Record<string, IImageCacheStatus>>;
|
|
59
|
+
declare function resolveAssetSource(source: IImageSourceProp): unknown;
|
|
60
|
+
export type IImageStatics = {
|
|
61
|
+
getSize: typeof getSize;
|
|
62
|
+
getSizeWithHeaders: typeof getSizeWithHeaders;
|
|
63
|
+
prefetch: typeof prefetch;
|
|
64
|
+
abortPrefetch: typeof abortPrefetch;
|
|
65
|
+
queryCache: typeof queryCache;
|
|
66
|
+
resolveAssetSource: typeof resolveAssetSource;
|
|
67
|
+
};
|
|
68
|
+
export declare const imageStatics: IImageStatics;
|
|
69
|
+
export type IImageViewProps = {
|
|
70
|
+
source?: IImageSourceProp;
|
|
71
|
+
defaultSource?: IImageSourceProp;
|
|
72
|
+
loadingIndicatorSource?: IImageSourceProp;
|
|
73
|
+
style?: IStyleProp<IViewStyle>;
|
|
74
|
+
resizeMode?: IResizeMode;
|
|
75
|
+
tintColor?: string;
|
|
76
|
+
src?: string;
|
|
77
|
+
srcSet?: string;
|
|
78
|
+
alt?: string;
|
|
79
|
+
width?: number;
|
|
80
|
+
height?: number;
|
|
81
|
+
crossOrigin?: 'anonymous' | 'use-credentials';
|
|
82
|
+
referrerPolicy?: string;
|
|
83
|
+
passthrough: Record<string, unknown>;
|
|
84
|
+
};
|
|
85
|
+
export declare function renderImage(view: IImageViewProps): IDescriptor;
|
|
86
|
+
export {};
|