@symbiote-native/svelte 1.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +11 -13
  2. package/build/components/{activity-indicator/activity-indicator-props.d.ts → activity-indicator-props.d.ts} +1 -1
  3. package/build/components/flat-list/index.svelte +9 -5
  4. package/build/components/{image-background/image-background-props.d.ts → image-background-props.d.ts} +1 -1
  5. package/build/components/index.d.ts +6 -24
  6. package/build/components/index.js +0 -18
  7. package/build/components/keyboard-avoiding-view/index.svelte +16 -7
  8. package/build/components/modal/index.svelte +7 -7
  9. package/build/components/pressable/pressable-props.d.ts +3 -0
  10. package/build/components/refresh-control-props.d.ts +1 -0
  11. package/build/components/text-props.d.ts +1 -0
  12. package/build/components/touchable-highlight/touchable-highlight-props.d.ts +2 -0
  13. package/build/components/touchable-native-feedback/touchable-native-feedback-props.d.ts +1 -0
  14. package/build/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +3 -1
  15. package/build/components/virtualized-list/index.svelte +79 -257
  16. package/build/components/virtualized-list/virtualized-list-props.d.ts +2 -2
  17. package/build/components/virtualized-list/virtualized-list-props.js +24 -4
  18. package/build/create-portal/index.js +2 -2
  19. package/build/dom-shim/canonical-prop-names.d.ts +2 -0
  20. package/build/dom-shim/canonical-prop-names.js +323 -0
  21. package/build/dom-shim/document.js +1 -1
  22. package/build/dom-shim/element.d.ts +10 -3
  23. package/build/dom-shim/element.js +233 -31
  24. package/build/dom-shim/patch-globals.js +31 -5
  25. package/build/dom-shim/shim-node.d.ts +6 -0
  26. package/build/dom-shim/shim-node.js +48 -1
  27. package/build/dom-shim/style-cache.d.ts +1 -0
  28. package/build/dom-shim/style-cache.js +27 -0
  29. package/build/dom-shim/text.js +5 -4
  30. package/build/host-instance.d.ts +2 -3
  31. package/build/host-instance.js +9 -3
  32. package/build/index.d.ts +7 -4
  33. package/build/index.js +17 -5
  34. package/build/intrinsic-elements.d.ts +63 -0
  35. package/build/modules/animated/index.d.ts +30 -46
  36. package/build/modules/animated/index.js +24 -34
  37. package/build/modules/image/index.d.ts +3 -0
  38. package/build/modules/image/index.js +15 -0
  39. package/build/preprocessor/scoped-styles.js +1 -1
  40. package/build/register.d.ts +1 -1
  41. package/build/register.js +13 -31
  42. package/build/root-element.js +1 -1
  43. package/metro-css-parser.cjs +2 -1
  44. package/metro-svelte-transformer.cjs +1 -17
  45. package/package.json +8 -16
  46. package/build/components/RefreshControl.svelte +0 -40
  47. package/build/components/RefreshControl.svelte.d.ts +0 -5
  48. package/build/components/SafeAreaView.svelte +0 -34
  49. package/build/components/SafeAreaView.svelte.d.ts +0 -5
  50. package/build/components/Text.svelte +0 -34
  51. package/build/components/Text.svelte.d.ts +0 -5
  52. package/build/components/View.svelte +0 -43
  53. package/build/components/View.svelte.d.ts +0 -5
  54. package/build/components/activity-indicator/activity-indicator-platform.android.d.ts +0 -3
  55. package/build/components/activity-indicator/activity-indicator-platform.android.js +0 -5
  56. package/build/components/activity-indicator/activity-indicator-platform.d.ts +0 -1
  57. package/build/components/activity-indicator/activity-indicator-platform.ios.d.ts +0 -3
  58. package/build/components/activity-indicator/activity-indicator-platform.ios.js +0 -6
  59. package/build/components/activity-indicator/activity-indicator-platform.js +0 -5
  60. package/build/components/activity-indicator/index.svelte +0 -63
  61. package/build/components/activity-indicator/index.svelte.d.ts +0 -5
  62. package/build/components/button.svelte +0 -76
  63. package/build/components/button.svelte.d.ts +0 -5
  64. package/build/components/image/image-logic.d.ts +0 -2
  65. package/build/components/image/image-logic.js +0 -135
  66. package/build/components/image/index.d.ts +0 -3
  67. package/build/components/image/index.js +0 -10
  68. package/build/components/image/index.svelte +0 -66
  69. package/build/components/image/index.svelte.d.ts +0 -5
  70. package/build/components/image-background/index.svelte +0 -87
  71. package/build/components/image-background/index.svelte.d.ts +0 -5
  72. package/build/components/input-accessory-view/index.svelte +0 -51
  73. package/build/components/input-accessory-view/index.svelte.d.ts +0 -5
  74. package/build/components/pressable/index.svelte +0 -185
  75. package/build/components/pressable/index.svelte.d.ts +0 -9
  76. package/build/components/scroll-view/index.svelte +0 -331
  77. package/build/components/scroll-view/index.svelte.d.ts +0 -18
  78. package/build/components/scroll-view/scroll-view-platform-types.d.ts +0 -3
  79. package/build/components/scroll-view/scroll-view-platform.android.d.ts +0 -3
  80. package/build/components/scroll-view/scroll-view-platform.android.js +0 -3
  81. package/build/components/scroll-view/scroll-view-platform.d.ts +0 -1
  82. package/build/components/scroll-view/scroll-view-platform.ios.d.ts +0 -3
  83. package/build/components/scroll-view/scroll-view-platform.ios.js +0 -3
  84. package/build/components/scroll-view/scroll-view-platform.js +0 -4
  85. package/build/components/scroll-view/scroll-view-sticky-context.d.ts +0 -7
  86. package/build/components/scroll-view/scroll-view-sticky-context.js +0 -1
  87. package/build/components/scroll-view/sticky-header-props.d.ts +0 -10
  88. package/build/components/scroll-view/sticky-header-props.js +0 -1
  89. package/build/components/scroll-view/sticky-header.svelte +0 -287
  90. package/build/components/scroll-view/sticky-header.svelte.d.ts +0 -5
  91. package/build/components/switch/index.svelte +0 -134
  92. package/build/components/switch/index.svelte.d.ts +0 -5
  93. package/build/components/text-input/index.svelte +0 -372
  94. package/build/components/text-input/index.svelte.d.ts +0 -17
  95. package/build/components/touchable-highlight/index.svelte +0 -139
  96. package/build/components/touchable-highlight/index.svelte.d.ts +0 -5
  97. package/build/components/touchable-native-feedback/index.d.ts +0 -9
  98. package/build/components/touchable-native-feedback/index.js +0 -14
  99. package/build/components/touchable-native-feedback/touchable-native-feedback.svelte +0 -49
  100. package/build/components/touchable-native-feedback/touchable-native-feedback.svelte.d.ts +0 -5
  101. package/build/components/touchable-opacity/index.svelte +0 -181
  102. package/build/components/touchable-opacity/index.svelte.d.ts +0 -5
  103. package/build/components/touchable-without-feedback/index.svelte +0 -75
  104. package/build/components/touchable-without-feedback/index.svelte.d.ts +0 -5
  105. package/build/dom-shim/fold-host-bag.d.ts +0 -1
  106. package/build/dom-shim/fold-host-bag.js +0 -7
  107. package/build/modules/animated/animated-component-props.d.ts +0 -7
  108. package/build/modules/animated/animated-component-props.js +0 -1
  109. package/build/modules/animated/create-animated-component.d.ts +0 -5
  110. package/build/modules/animated/create-animated-component.js +0 -131
  111. package/build/preprocessor/lower-host-primitives.d.ts +0 -10
  112. package/build/preprocessor/lower-host-primitives.js +0 -455
  113. package/build/state-style.d.ts +0 -1
  114. package/build/state-style.js +0 -8
  115. /package/build/components/{activity-indicator/activity-indicator-props.js → activity-indicator-props.js} +0 -0
  116. /package/build/components/{image-background/image-background-props.js → image-background-props.js} +0 -0
  117. /package/build/{components/scroll-view/scroll-view-platform-types.js → intrinsic-elements.js} +0 -0
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" module>
2
2
  // VirtualizedList: real windowing over a hand-authored minimal scroll host. Only cells whose
3
3
  // computed offset falls inside the visible window (plus a leading/trailing buffer) render;
4
- // the rest collapses into two spacer symbiote-view nodes.
4
+ // the rest collapses into two spacer view nodes.
5
5
  //
6
6
  // The orchestration - window recompute, edge-reached, viewability, batch fill, MVCP, imperative
7
7
  // scrolls - is the framework-agnostic `reduceList` state machine in @symbiote-native/components
@@ -10,16 +10,22 @@
10
10
  // (`listState`), runs the returned EFFECTS with Svelte primitives, and renders the windowed slice
11
11
  // with `{#each plan.cells}` (Lists have no Descriptor render fn, per svelte-adapter-dom-shim §15).
12
12
  //
13
- // This file hand-authors the raw `symbiote-scroll-view`/`symbiote-scroll-content` intrinsics
14
- // directly rather than rendering <ScrollView>: unlike ScrollView.svelte, it walks an indexable
15
- // `plan.cells` list instead of an opaque children Snippet, so it can auto-wrap sticky cells
16
- // itself. `onContentSizeChange` remains the one gap left from that; nestedScrollEnabled, the
17
- // Android RefreshControl wrap style-split, and JS sticky-header wrapping are all wired directly
18
- // here instead.
13
+ // This file authors the raw `scroll-view` intrinsic directly, and needs to: it walks an indexable
14
+ // `plan.cells` list rather than taking an opaque children Snippet, so it can mark sticky cells
15
+ // itself. (There is no ScrollView component to render instead — it was deleted 2026-09-10 and an
16
+ // app writes the tag too.)
19
17
  //
20
- // RefreshControl attaches directly to these raw scroll intrinsics: a sibling before the content
21
- // on iOS, wrapping the whole scroll view on Android (PLATFORM.refreshControlMode) - the same
22
- // shape ScrollView's own index.svelte uses.
18
+ // It does NOT author the CONTENT node. `registerScrollViewBehavior()` puts a `buildStructure` on
19
+ // the scroll tags, and exactly one thing may build `RCTScrollContentView` — emitting one here as
20
+ // well nests a second one inside the engine's. Everything that used to live here because the
21
+ // content node did (`contentContainerStyle`, the axis base style, `nestedScrollEnabled`, the
22
+ // Android RefreshControl wrap style-split, the JS sticky-header wrapping and its collision map)
23
+ // is the behavior's now; `contentContainerStyle` travels as an OWNER prop and the behavior
24
+ // renames it onto the slot.
25
+ //
26
+ // RefreshControl is written as an ordinary child of the scroll tag on both platforms: the
27
+ // behavior CLAIMS it, keeping it beside the content view on iOS and wrapping the scroll view
28
+ // with it on Android.
23
29
  //
24
30
  // `<svelte:element>` is forbidden (svelte-adapter-dom-shim skill §4/§7 - its shim surface isn't
25
31
  // implemented), so the horizontal/vertical choice is two static branches sharing one
@@ -34,34 +40,25 @@
34
40
 
35
41
  <script lang="ts" generics="ItemT">
36
42
  import {
37
- DEFAULT_END_REACHED_THRESHOLD,
38
43
  DEFAULT_INITIAL_NUM_TO_RENDER,
39
44
  DEFAULT_MAX_TO_RENDER_PER_BATCH,
40
- DEFAULT_START_REACHED_THRESHOLD,
41
45
  DEFAULT_UPDATE_CELLS_BATCHING_PERIOD,
42
46
  DEFAULT_WINDOW_SIZE,
43
47
  EMPTY_OFFSET,
44
48
  FIRST_INDEX,
45
49
  INVERTED_X_STYLE,
46
50
  INVERTED_Y_STYLE,
47
- attachStickyScroll,
48
51
  buildListPlan,
49
52
  buildScrollViewHandle,
50
53
  buildViewabilityPairs,
51
54
  createInitialListState,
52
- forwardScrollEvent,
53
55
  isSeparatorGapInRange,
54
56
  listEffectSignature,
55
- nextStickyHeaderY,
56
57
  readLayoutLength,
57
58
  readLayoutOffset,
58
- readLayoutNumber,
59
59
  readScrollOffset,
60
60
  reduceList,
61
61
  resolveItemKey,
62
- resolveScrollForwarding,
63
- selectScrollIntrinsics,
64
- splitLayoutProps,
65
62
  type IListAction,
66
63
  type IListEffect,
67
64
  type IListReducerInputs,
@@ -71,24 +68,11 @@
71
68
  type ISeparators,
72
69
  } from '@symbiote-native/components';
73
70
  import {
74
- AnimatedValue,
75
71
  dlog,
76
- event as animatedEvent,
77
- isNativeAnimatedAvailable,
78
- resolveClassName,
79
72
  type ISymbioteEvent,
80
73
  type ISymbioteNode,
81
74
  } from '@symbiote-native/engine';
82
- import { resolveSvelteClass } from '../../class-value';
83
- import { setContext } from 'svelte';
84
75
  import type { ShimElement } from '../../dom-shim';
85
- import RefreshControl from '../RefreshControl.svelte';
86
- import { PLATFORM } from '../scroll-view/scroll-view-platform';
87
- import ScrollViewStickyHeader from '../scroll-view/sticky-header.svelte';
88
- import {
89
- SCROLL_VIEW_STICKY_CONTEXT_KEY,
90
- type IScrollViewStickyContext,
91
- } from '../scroll-view/scroll-view-sticky-context';
92
76
  import {
93
77
  pickAccessibilityProps,
94
78
  type IVirtualizedListProps as IProps,
@@ -129,22 +113,6 @@
129
113
  let viewableTimer: ReturnType<typeof setTimeout> | null = null;
130
114
  let batchTimer: ReturnType<typeof setTimeout> | null = null;
131
115
 
132
- // Sticky headers: this file hand-rolls the raw scroll intrinsic (see the header comment), so it
133
- // can't lean on ScrollView.svelte's sticky wiring directly - it mirrors the same
134
- // scrollAnimatedValue/attachStickyScroll/context wiring here, applied per windowed cell.
135
- const scrollAnimatedValue = new AnimatedValue(0);
136
- let viewportHeight = $state<number | undefined>(undefined);
137
- // y of each measured sticky header, keyed by ORIGINAL list index (RN's _headerLayoutYs) - plain,
138
- // not $state; stickyVersion (below) drives re-derivation of the collision math that reads it.
139
- const headerLayoutYs = new Map<number, number>();
140
- let stickyVersion = $state(0);
141
-
142
- setContext<IScrollViewStickyContext>(SCROLL_VIEW_STICKY_CONTEXT_KEY, {
143
- scrollAnimatedValue,
144
- getInverted: () => narrowed.inverted,
145
- getViewportHeight: () => viewportHeight,
146
- });
147
-
148
116
  const narrowed = $derived.by(() => {
149
117
  // extraData has no field of its own; reading it tracks it so a change forces this derived to
150
118
  // re-run (RN's extraData contract).
@@ -158,11 +126,9 @@
158
126
  horizontal: props.horizontal === true,
159
127
  inverted: props.inverted === true,
160
128
  onEndReached: props.onEndReached,
161
- onEndReachedThreshold:
162
- props.onEndReachedThreshold ?? DEFAULT_END_REACHED_THRESHOLD,
129
+ onEndReachedThreshold: props.onEndReachedThreshold,
163
130
  onStartReached: props.onStartReached,
164
- onStartReachedThreshold:
165
- props.onStartReachedThreshold ?? DEFAULT_START_REACHED_THRESHOLD,
131
+ onStartReachedThreshold: props.onStartReachedThreshold,
166
132
  onRefresh: props.onRefresh,
167
133
  refreshing: props.refreshing,
168
134
  progressViewOffset: props.progressViewOffset,
@@ -282,7 +248,12 @@
282
248
  const info = effect.info;
283
249
  const map = effect.map;
284
250
  const fire = (): void => {
285
- for (const pair of pairs) pair.onViewableItemsChanged(info);
251
+ for (const pair of pairs) {
252
+ pair.onViewableItemsChanged({
253
+ ...info,
254
+ viewabilityConfig: pair.viewabilityConfig,
255
+ });
256
+ }
286
257
  dispatch({ kind: 'viewable-fired', map });
287
258
  };
288
259
  if (viewableTimer !== null) {
@@ -323,58 +294,6 @@
323
294
  return listEffectSignature(listState);
324
295
  });
325
296
 
326
- const hasStickyHeaders = $derived(
327
- narrowed.stickyHeaderIndices !== undefined &&
328
- narrowed.stickyHeaderIndices.length > 0,
329
- );
330
- // Resolved dynamically, exactly like React (adapters/react/.../scroll-view/shared.ts:267). Do
331
- // not hardcode this false to keep the JS listener alive - RN gates native the same way
332
- // (AnimatedWithChildren.js:74 `if (!this.__isNative)`) and the pin is the native transform, not
333
- // the listener, so forcing JS mode only trades away the native driver: header lag on iOS,
334
- // outright failure on Android (commit debounce 15ms vs iOS's 64ms - render-scroll-sticky.ts).
335
- const nativeStickyAvailable = $derived(
336
- hasStickyHeaders && isNativeAnimatedAvailable(),
337
- );
338
- // RN's FlatList/SectionList expose no invertStickyHeaders of their own (only ScrollView does) -
339
- // sticky headers always pin to the top here, matching RN.
340
- const forwarding = $derived(
341
- resolveScrollForwarding({
342
- hasStickyHeaders,
343
- nativeStickyAvailable,
344
- invertStickyHeaders: undefined,
345
- scrollEventThrottle: narrowed.scrollEventThrottle,
346
- maintainVisibleContentPosition: narrowed.maintainVisibleContentPosition,
347
- snapToAlignment: undefined,
348
- }),
349
- );
350
-
351
- $effect(() => {
352
- dlog(
353
- `VirtualizedList sticky hasStickyHeaders=${hasStickyHeaders} nativeStickyAvailable=${nativeStickyAvailable} mode=${forwarding.mode} scrollEventThrottle=${String(forwarding.scrollEventThrottle)}`,
354
- );
355
- });
356
-
357
- // Drive the sticky scroll value on the native UI thread once the scroll host commits (mirrors
358
- // ScrollView.svelte). No-op without the native animated module - the JS fallback below still
359
- // keeps headers pinned via onScroll.
360
- $effect(() => {
361
- if (!nativeStickyAvailable) {
362
- dlog(
363
- 'VirtualizedList sticky attachStickyScroll skipped: nativeStickyAvailable=false',
364
- );
365
- return;
366
- }
367
- const node = hostShim?.engineNode;
368
- if (node === undefined) {
369
- dlog(
370
- 'VirtualizedList sticky attachStickyScroll skipped: engineNode not ready yet',
371
- );
372
- return;
373
- }
374
- dlog('VirtualizedList sticky attachStickyScroll attached');
375
- return attachStickyScroll(node, scrollAnimatedValue);
376
- });
377
-
378
297
  function handleScroll(event: ISymbioteEvent): void {
379
298
  const offset = readScrollOffset(event, narrowed.horizontal);
380
299
  if (offset === undefined) return;
@@ -384,66 +303,11 @@
384
303
  narrowed.userOnScroll?.(event);
385
304
  }
386
305
 
387
- // sticky-js: no native Animated module, so the scroll value is driven off the JS thread via the
388
- // same wrapped-listener shape ScrollView.svelte uses. sticky-native/plain: forward untouched.
389
- const onScroll = $derived.by(() => {
390
- if (forwarding.mode !== 'sticky-js') return handleScroll;
391
- return animatedEvent(
392
- [{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }],
393
- {
394
- listener: (...args: unknown[]) =>
395
- forwardScrollEvent(handleScroll, args),
396
- },
397
- );
398
- });
399
-
400
306
  function onViewportLayout(event: ISymbioteEvent): void {
401
307
  const length = readLayoutLength(event, narrowed.horizontal);
402
308
  if (length === undefined) return;
403
309
  dlog(`VirtualizedList onLayout viewport=${length}`);
404
310
  dispatch({ kind: 'layout', length });
405
- if (forwarding.capturesViewportHeight) {
406
- const height = readLayoutNumber(event, 'height');
407
- if (height !== undefined) viewportHeight = height;
408
- }
409
- }
410
-
411
- // Records a sticky header's measured y (RN's _headerLayoutYs) so the NEXT sticky header down the
412
- // list can compute its collision point - without this every header would stick indefinitely
413
- // instead of being pushed off by the one behind it.
414
- function recordHeaderY(index: number, event: ISymbioteEvent): void {
415
- const y = readLayoutNumber(event, 'y');
416
- if (y === undefined) return;
417
- // This map is the ONLY source of nextHeaderLayoutY for every header ahead of this one; if a
418
- // later header's onLayout never fires (windowing drops its cell) the collision math upstream
419
- // falls back to `?? 0` (collisionPoint = -layoutHeight), which is almost certainly wrong.
420
- dlog(`VirtualizedList recordHeaderY index=${index} y=${y}`);
421
- headerLayoutYs.set(index, y);
422
- stickyVersion += 1;
423
- }
424
-
425
- function stickyLayoutFor(index: number): (event: ISymbioteEvent) => void {
426
- const measure = makeCellMeasure(index);
427
- return (event: ISymbioteEvent): void => {
428
- measure(event);
429
- recordHeaderY(index, event);
430
- };
431
- }
432
-
433
- // Only a header MOUNTED RIGHT NOW can collide with this one, matching React (its ScrollView
434
- // receives `renderedStickyIndices`, not the full section list -
435
- // adapters/react/.../virtualized-list/index.ts:732). Passing the full index list instead let a
436
- // header collide against a stale y from one that had already scrolled out and unmounted,
437
- // freezing it in place - proven by sticky-collision-parity.test.ts against the React reference.
438
- // The map itself is deliberately NOT pruned (React doesn't either): a measured y stays valid for
439
- // when that header scrolls back in.
440
- function nextStickyHeaderYFor(index: number): number | undefined {
441
- void stickyVersion;
442
- const indices = narrowed.stickyHeaderIndices;
443
- if (indices === undefined) return undefined;
444
- const mounted = new Set(allCells.map(cell => cell.index));
445
- const rendered = indices.filter(stickyIndex => mounted.has(stickyIndex));
446
- return nextStickyHeaderY(rendered, rendered.indexOf(index), headerLayoutYs);
447
311
  }
448
312
 
449
313
  function makeCellMeasure(index: number): (event: ISymbioteEvent) => void {
@@ -573,10 +437,6 @@
573
437
  };
574
438
  });
575
439
 
576
- const scrollViewIntrinsics = $derived(
577
- selectScrollIntrinsics(narrowed.horizontal, narrowed.contentContainerStyle),
578
- );
579
-
580
440
  const resolvedStyle = $derived(
581
441
  narrowed.inverted
582
442
  ? [
@@ -585,44 +445,30 @@
585
445
  ]
586
446
  : narrowed.style,
587
447
  );
448
+ // Travels on the OWNER: `slotProps` renames it onto the content node the behavior built, so it
449
+ // goes through the slot's own routeProp and inherits class resolution and style merging. The
450
+ // horizontal `flexDirection: 'row'` is the behavior's constant and is composed OVER this, so
451
+ // only the measured total width is added here.
588
452
  const resolvedContentContainerStyle = $derived(
589
453
  narrowed.horizontal
590
- ? [scrollViewIntrinsics.contentStyle, { width: metrics.total }]
591
- : scrollViewIntrinsics.contentStyle,
592
- );
593
-
594
- // Android wrap mode only: mirrors ScrollView's own index.svelte - RN's splitLayoutProps routes
595
- // LAYOUT props (margin/flex/size/position/...) onto the wrapping AndroidSwipeRefreshLayout,
596
- // VISUAL props (background/padding/border/...) onto the inner scroll view, instead of leaving
597
- // the wrapper unstyled (which collapses it to zero height).
598
- const layoutSplit = $derived(
599
- shouldWrapRefreshControl
600
- ? splitLayoutProps([resolveSvelteClass(narrowed.class), resolvedStyle])
601
- : undefined,
454
+ ? [narrowed.contentContainerStyle, { width: metrics.total }]
455
+ : narrowed.contentContainerStyle,
602
456
  );
603
457
 
604
458
  const outerBag = $derived.by(() => {
605
459
  const bag: Record<string, unknown> = {
606
- style: [
607
- scrollViewIntrinsics.scrollViewBaseStyle,
608
- layoutSplit !== undefined ? layoutSplit.inner : resolvedStyle,
609
- ],
610
- horizontal: narrowed.horizontal,
611
- // RN defaults nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`). This file
612
- // hand-rolls the raw scroll intrinsic instead of rendering <ScrollView>, so it never
613
- // inherited that default - on Android, a list nested inside a page ScrollView never got
614
- // nested-scroll gesture arbitration and only the outer page scrolled.
615
- nestedScrollEnabled: true,
616
- onScroll,
460
+ style: resolvedStyle,
461
+ class: narrowed.class,
462
+ contentContainerStyle: resolvedContentContainerStyle,
463
+ onScroll: handleScroll,
617
464
  onLayout: onViewportLayout,
618
465
  };
619
- if (layoutSplit === undefined) bag.class = narrowed.class;
620
466
  if (commandedOffset !== undefined) bag.contentOffset = commandedOffset;
621
- // forwarding.scrollEventThrottle (not the raw prop): folds in the sticky-mode default
622
- // (1 native / 16 JS-fallback) when unset - without it a sticky header rebuilt off too-sparse
623
- // scroll events pins/collides late.
624
- if (forwarding.scrollEventThrottle !== undefined)
625
- bag.scrollEventThrottle = forwarding.scrollEventThrottle;
467
+ // The RAW prop. A sticky header raises the throttle from the behavior, once it has actually
468
+ // registered — folding a sticky default in here would hand the behavior a number it reads back
469
+ // as the app's and could never take away again.
470
+ if (narrowed.scrollEventThrottle !== undefined)
471
+ bag.scrollEventThrottle = narrowed.scrollEventThrottle;
626
472
  if (narrowed.onScrollBeginDrag !== undefined)
627
473
  bag.onScrollBeginDrag = narrowed.onScrollBeginDrag;
628
474
  if (narrowed.onScrollEndDrag !== undefined)
@@ -636,8 +482,11 @@
636
482
  }
637
483
  if (narrowed.keyboardDismissMode !== undefined)
638
484
  bag.keyboardDismissMode = narrowed.keyboardDismissMode;
639
- if (narrowed.stickyHeaderIndices !== undefined)
640
- bag.stickyHeaderIndices = narrowed.stickyHeaderIndices;
485
+ // `stickyHeaderIndices` is deliberately NOT forwarded. The behavior honours it by numbering the
486
+ // owner's own PAINT children, and this list's indices are into the DATA stream — a windowed
487
+ // list paints a spacer, a header and a slice, so index 3 of the data is almost never paint
488
+ // child 3. The cells that should pin carry the `sticky-header` tag directly instead, which is
489
+ // the one form that survives windowing.
641
490
  if (narrowed.maintainVisibleContentPosition !== undefined) {
642
491
  bag.maintainVisibleContentPosition =
643
492
  narrowed.maintainVisibleContentPosition;
@@ -659,18 +508,6 @@
659
508
  progressViewOffset: narrowed.progressViewOffset,
660
509
  };
661
510
  });
662
- // iOS: RefreshControl is a childless sibling before the content container. Android: it WRAPS the
663
- // whole scroll view (an Android ScrollView accepts only one child) - same PLATFORM.
664
- // refreshControlMode ScrollView's own index.svelte reads.
665
- const shouldWrapRefreshControl = $derived(
666
- PLATFORM.refreshControlMode === 'wrap' && refreshControlProps !== undefined,
667
- );
668
-
669
- const contentBag = $derived({
670
- style: resolvedContentContainerStyle,
671
- collapsable: false,
672
- });
673
-
674
511
  const stickySet = $derived(
675
512
  narrowed.stickyHeaderIndices !== undefined
676
513
  ? new Set(narrowed.stickyHeaderIndices)
@@ -740,19 +577,19 @@
740
577
  lets virtualized-list.smoke.test.ts still assert an exact windowed child count.
741
578
  -->
742
579
  {#if hasHeader}
743
- <symbiote-view p={{}}>
580
+ <view p={{}}>
744
581
  {@render props.header?.()}
745
- </symbiote-view>
582
+ </view>
746
583
  {/if}
747
584
  {#if metrics.count === FIRST_INDEX}
748
585
  {#if props.empty}
749
- <symbiote-view p={{}}>
586
+ <view p={{}}>
750
587
  {@render props.empty()}
751
- </symbiote-view>
588
+ </view>
752
589
  {/if}
753
590
  {:else if plan}
754
591
  {#if plan.leadingExtent > EMPTY_OFFSET}
755
- <symbiote-view
592
+ <view
756
593
  p={{
757
594
  style: narrowed.horizontal
758
595
  ? { width: plan.leadingExtent }
@@ -762,23 +599,24 @@
762
599
  {/if}
763
600
  {#each allCells as cell (cell.key)}
764
601
  {#if stickySet?.has(cell.index)}
765
- <ScrollViewStickyHeader
766
- onLayout={stickyLayoutFor(cell.index)}
767
- nextHeaderLayoutY={nextStickyHeaderYFor(cell.index)}
768
- >
602
+ <!-- The TAG, not a component: `registerScrollViewBehavior()` registers `sticky-header`
603
+ alongside the scroll tags, and its behavior finds this ScrollView by walking up. The
604
+ collision point comes from the owner's DOCUMENT order, so nothing here computes or
605
+ forwards an index. `onLayout` is forwarded by the behavior, not replaced. -->
606
+ <sticky-header p={{ onLayout: makeCellMeasure(cell.index) }}>
769
607
  {@render props.item({
770
608
  item: narrowed.getItem(narrowed.data, cell.index),
771
609
  index: cell.index,
772
610
  separators: makeSeparators(cell.index),
773
611
  })}
774
612
  {#if props.separator && cell.index < metrics.count - 1}
775
- <symbiote-view p={{}}>
613
+ <view p={{}}>
776
614
  {@render props.separator(separatorPropsFor(cell.index))}
777
- </symbiote-view>
615
+ </view>
778
616
  {/if}
779
- </ScrollViewStickyHeader>
617
+ </sticky-header>
780
618
  {:else}
781
- <symbiote-view
619
+ <view
782
620
  p={{
783
621
  onLayout: makeCellMeasure(cell.index),
784
622
  style: cellInvertedStyle,
@@ -790,14 +628,14 @@
790
628
  separators: makeSeparators(cell.index),
791
629
  })}
792
630
  {#if props.separator && cell.index < metrics.count - 1}
793
- <symbiote-view p={{}}>
631
+ <view p={{}}>
794
632
  {@render props.separator(separatorPropsFor(cell.index))}
795
- </symbiote-view>
633
+ </view>
796
634
  {/if}
797
- </symbiote-view>
635
+ </view>
798
636
  {/if}
799
637
  {#if plan.forcedStickyCell && cell.index === plan.forcedStickyCell.index && plan.gapExtent > EMPTY_OFFSET}
800
- <symbiote-view
638
+ <view
801
639
  p={{
802
640
  style: narrowed.horizontal
803
641
  ? { width: plan.gapExtent }
@@ -807,7 +645,7 @@
807
645
  {/if}
808
646
  {/each}
809
647
  {#if plan.trailingExtent > EMPTY_OFFSET}
810
- <symbiote-view
648
+ <view
811
649
  p={{
812
650
  style: narrowed.horizontal
813
651
  ? { width: plan.trailingExtent }
@@ -817,49 +655,33 @@
817
655
  {/if}
818
656
  {/if}
819
657
  {#if props.footer}
820
- <symbiote-view p={{}}>
658
+ <view p={{}}>
821
659
  {@render props.footer()}
822
- </symbiote-view>
660
+ </view>
823
661
  {/if}
824
662
  {/snippet}
825
663
 
826
664
  {#snippet scrollBody()}
827
665
  <!--
828
- Same as listBody() above: on iOS the non-wrapping RefreshControl and the content container are
829
- siblings of one parent, and the gap between them is dropped before it reaches Fabric.
666
+ Same as listBody() above: the RefreshControl and the cells are siblings of one parent here, and
667
+ the whitespace between them is dropped before it reaches Fabric.
668
+
669
+ RefreshControl is written as an ordinary child on BOTH platforms — the behavior CLAIMS it, so
670
+ the engine keeps it beside the content view on iOS and inverts the tree on Android. Everything
671
+ after it lands in the content node the behavior built.
830
672
  -->
831
- {#if !shouldWrapRefreshControl && refreshControlProps !== undefined}
832
- <RefreshControl {...refreshControlProps} />
833
- {/if}
834
- {#if narrowed.horizontal}
835
- <symbiote-horizontal-scroll-content p={contentBag}>
836
- {@render listBody()}
837
- </symbiote-horizontal-scroll-content>
838
- {:else}
839
- <symbiote-scroll-content p={contentBag}>
840
- {@render listBody()}
841
- </symbiote-scroll-content>
673
+ {#if refreshControlProps !== undefined}
674
+ <refresh-control p={refreshControlProps} />
842
675
  {/if}
676
+ {@render listBody()}
843
677
  {/snippet}
844
678
 
845
- {#if shouldWrapRefreshControl && refreshControlProps !== undefined}
846
- <RefreshControl {...refreshControlProps} style={layoutSplit?.outer}>
847
- {#if narrowed.horizontal}
848
- <symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>
849
- {@render scrollBody()}
850
- </symbiote-horizontal-scroll-view>
851
- {:else}
852
- <symbiote-scroll-view p={outerBag} bind:this={hostShim}>
853
- {@render scrollBody()}
854
- </symbiote-scroll-view>
855
- {/if}
856
- </RefreshControl>
857
- {:else if narrowed.horizontal}
858
- <symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>
679
+ {#if narrowed.horizontal}
680
+ <horizontal-scroll-view p={outerBag} bind:this={hostShim}>
859
681
  {@render scrollBody()}
860
- </symbiote-horizontal-scroll-view>
682
+ </horizontal-scroll-view>
861
683
  {:else}
862
- <symbiote-scroll-view p={outerBag} bind:this={hostShim}>
684
+ <scroll-view p={outerBag} bind:this={hostShim}>
863
685
  {@render scrollBody()}
864
- </symbiote-scroll-view>
686
+ </scroll-view>
865
687
  {/if}
@@ -1,5 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { IStyleProp, ISymbioteEvent, ISymbioteNode, IViewStyle } from '@symbiote-native/engine';
2
+ import { type IStyleProp, type ISymbioteEvent, type ISymbioteNode, type IViewStyle } from '@symbiote-native/engine';
3
3
  import { type IAccessibilityProps, type IAriaProps, type ISeparatorProps, type ISeparators, type IViewabilityConfig, type IViewabilityConfigCallbackPair, type IViewableItemsChangedInfo, type IVirtualizedListHandle } from '@symbiote-native/components';
4
4
  import type { ISvelteClassValue } from '../../class-value';
5
5
  export type { IVirtualizedListHandle };
@@ -67,4 +67,4 @@ export interface IVirtualizedListProps<ItemT> extends IAccessibilityProps, IAria
67
67
  class?: ISvelteClassValue;
68
68
  }
69
69
  export type { ISeparators, ISeparatorProps, ISymbioteEvent, ISymbioteNode };
70
- export declare function pickAccessibilityProps<T extends IAccessibilityProps & IAriaProps>(props: T): IAccessibilityProps;
70
+ export declare function pickAccessibilityProps<T extends IAccessibilityProps & IAriaProps>(props: T): IAccessibilityProps & IAriaProps;
@@ -1,14 +1,18 @@
1
- import { resolveAccessibilityProps, } from '@symbiote-native/components';
1
+ import { ARIA_ALIAS_KEYS, } from '@symbiote-native/engine';
2
+ import {} from '@symbiote-native/components';
2
3
  // Every IAccessibilityProps field, named explicitly (the object-bag convention this adapter's
3
4
  // custom-element host tags require — svelte-adapter-dom-shim skill §3g(c) — forbids a raw
4
5
  // `...rest` spread landing on a symbiote-* tag, so this mirrors React's `...accessibilityRest`
5
6
  // field-by-field instead of reusing its spread). Shared by every list component (VirtualizedList's
6
7
  // own host-bag construction AND FlatList's/VirtualizedSectionList's/SectionList's component-to-
7
8
  // component forwarding down to VirtualizedList) so the field list lives in exactly one place.
8
- // resolveAccessibilityProps folds aria-*/role into their accessibility* twins first (idempotent —
9
- // calling it twice, once per forwarding hop, is a documented no-op once the aria keys are gone).
9
+ // It used to call `resolveAccessibilityProps` first, folding `aria-*`/`role` into their
10
+ // `accessibility*` twins so there were canonical names to pick BY NAME. That fold is the device's
11
+ // rule now, so the aria keys are picked and forwarded RAW instead (the loop at the end) and the
12
+ // engine folds once, at the leaf. Picking twice across a forwarding hop stays a no-op for the same
13
+ // reason folding twice was: a pick is idempotent.
10
14
  export function pickAccessibilityProps(props) {
11
- const resolved = resolveAccessibilityProps(props);
15
+ const resolved = props;
12
16
  const picked = {};
13
17
  if (resolved.testID !== undefined)
14
18
  picked.testID = resolved.testID;
@@ -73,5 +77,21 @@ export function pickAccessibilityProps(props) {
73
77
  picked.onMagicTap = resolved.onMagicTap;
74
78
  if (resolved.onAccessibilityEscape !== undefined)
75
79
  picked.onAccessibilityEscape = resolved.onAccessibilityEscape;
80
+ // THE ARIA HALF, forwarded RAW and DERIVED from the engine's own list rather than named again
81
+ // here. `ARIA_ALIAS_KEYS` is exported for exactly this — its comment says a second hand-written
82
+ // copy is what `adapter-parity-audit.md` records going stale one member at a time — so this loop
83
+ // gains a new alias the day the engine does.
84
+ //
85
+ // Raw, because the fold is the device's rule (`foldAriaProps`, `SymbioteFabricProps.cpp`) and it
86
+ // reads these names literally off the bag it commits. Folding them here would be a second
87
+ // implementation of it, and the one this file used to hold: `resolveAccessibilityProps`.
88
+ for (const key of ARIA_ALIAS_KEYS) {
89
+ const value = props[key];
90
+ // `Object.assign` with a computed key, not `picked[key] = value`: the key is a union and the
91
+ // value is its correlated member type, which TypeScript cannot check across a loop. This is the
92
+ // spelling that stays sound without an `as`.
93
+ if (value !== undefined)
94
+ Object.assign(picked, { [key]: value });
95
+ }
76
96
  return picked;
77
97
  }
@@ -55,7 +55,7 @@
55
55
  // Node identity | nodes are MOVED (the old | content is re-created per TunnelOut
56
56
  // | parent empties) |
57
57
  import { pop, push, snippet, user_effect } from 'svelte/internal/client';
58
- import { appendChild as engineAppendChild, removeChild as engineRemoveChild, dlog, isSymbioteNode, SymbioteSurface, } from '@symbiote-native/engine';
58
+ import { appendChild as engineAppendChild, removeChild as engineRemoveChild, componentOf, dlog, isSymbioteNode, SymbioteSurface, } from '@symbiote-native/engine';
59
59
  import { ShimComment, ShimElement, ShimNode } from '../dom-shim/index.js';
60
60
  // The Svelte flavour of React's `isSymbioteNode` guard, and it needs its own wording: React tells
61
61
  // the caller to check for a forgotten `.current`, which does not exist here. A Svelte host ref is
@@ -75,7 +75,7 @@ function targetLabel(target) {
75
75
  return `surface#${target.rootTag}`;
76
76
  if (target instanceof ShimElement)
77
77
  return target.tagName;
78
- return target.component;
78
+ return componentOf(target);
79
79
  }
80
80
  // Attach the fragment host under `target` and hand back the matching detach. Three branches
81
81
  // because the three target kinds sit at different layers:
@@ -0,0 +1,2 @@
1
+ export declare const CANONICAL_PROP_NAMES: readonly string[];
2
+ export declare const CANONICAL_BY_LOWER: ReadonlyMap<string, string>;