@symbiote-native/svelte 0.2.0 → 0.3.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 (67) hide show
  1. package/build/bootstrap.js +1 -1
  2. package/build/class-value.js +1 -1
  3. package/build/components/RefreshControl.svelte +4 -2
  4. package/build/components/Text.svelte +11 -2
  5. package/build/components/View.svelte +3 -1
  6. package/build/components/activity-indicator/index.svelte +8 -2
  7. package/build/components/button.svelte +8 -3
  8. package/build/components/flat-list/index.svelte +46 -11
  9. package/build/components/image/image-logic.js +13 -3
  10. package/build/components/image-background/index.svelte +8 -2
  11. package/build/components/index.d.ts +2 -2
  12. package/build/components/input-accessory-view/index.svelte +4 -1
  13. package/build/components/keyboard-avoiding-view/index.svelte +34 -11
  14. package/build/components/modal/index.svelte +4 -1
  15. package/build/components/pressable/index.svelte +10 -3
  16. package/build/components/pressable/pressable-props.d.ts +1 -1
  17. package/build/components/scroll-view/index.svelte +54 -21
  18. package/build/components/scroll-view/sticky-header.svelte +49 -16
  19. package/build/components/section-list/index.svelte +6 -1
  20. package/build/components/switch/index.svelte +14 -3
  21. package/build/components/text-input/index.svelte +22 -6
  22. package/build/components/touchable-highlight/index.svelte +114 -15
  23. package/build/components/touchable-highlight/touchable-highlight-props.d.ts +2 -0
  24. package/build/components/touchable-native-feedback/touchable-native-feedback.svelte +7 -2
  25. package/build/components/touchable-opacity/index.svelte +112 -56
  26. package/build/components/touchable-without-feedback/index.svelte +59 -4
  27. package/build/components/virtualized-list/index.svelte +233 -70
  28. package/build/components/virtualized-list/virtualized-list-props.js +8 -4
  29. package/build/components/virtualized-section-list/index.svelte +56 -9
  30. package/build/components/virtualized-section-list/index.svelte.d.ts +1 -1
  31. package/build/components/virtualized-section-list/virtualized-section-list-props.d.ts +5 -0
  32. package/build/create-portal/index.d.ts +22 -0
  33. package/build/create-portal/index.js +160 -0
  34. package/build/descriptor-to-svelte.js +15 -21
  35. package/build/dom-shim/index.d.ts +1 -0
  36. package/build/dom-shim/index.js +3 -0
  37. package/build/dom-shim/text.js +38 -1
  38. package/build/host-instance.js +7 -2
  39. package/build/index.d.ts +5 -3
  40. package/build/index.js +16 -11
  41. package/build/modules/animated/create-animated-component.d.ts +5 -0
  42. package/build/modules/animated/create-animated-component.js +131 -0
  43. package/build/modules/animated/index.d.ts +24 -14
  44. package/build/modules/animated/index.js +30 -30
  45. package/build/modules/status-bar/index.js +1 -1
  46. package/build/modules/status-bar/index.svelte +4 -1
  47. package/build/native-view-bridge.d.ts +1 -1
  48. package/build/native-view-bridge.js +1 -1
  49. package/build/preprocessor/collapse-text-whitespace.d.ts +10 -0
  50. package/build/preprocessor/collapse-text-whitespace.js +138 -0
  51. package/build/preprocessor/forbid-web-only-constructs.js +8 -2
  52. package/build/preprocessor/scoped-styles.js +141 -104
  53. package/build/render.js +48 -3
  54. package/build/scope-token.d.ts +3 -2
  55. package/build/scope-token.js +6 -17
  56. package/build/style-scope.d.ts +2 -1
  57. package/build/style-scope.js +10 -8
  58. package/metro-svelte-transformer.cjs +17 -2
  59. package/package.json +11 -7
  60. package/build/modules/animated/AnimatedImage.svelte +0 -115
  61. package/build/modules/animated/AnimatedImage.svelte.d.ts +0 -5
  62. package/build/modules/animated/AnimatedScrollView.svelte +0 -92
  63. package/build/modules/animated/AnimatedScrollView.svelte.d.ts +0 -17
  64. package/build/modules/animated/AnimatedText.svelte +0 -58
  65. package/build/modules/animated/AnimatedText.svelte.d.ts +0 -5
  66. package/build/modules/animated/AnimatedView.svelte +0 -95
  67. package/build/modules/animated/AnimatedView.svelte.d.ts +0 -5
@@ -3,7 +3,7 @@
3
3
  // own source is Flow syntax Vitest's transform can't parse, so anything importing it directly
4
4
  // must stay unreachable from the tested main index.ts.
5
5
  import { AppRegistry as RNAppRegistry } from 'react-native';
6
- import { bootstrapHost } from '@symbiote-native/components/bootstrap';
6
+ import { bootstrapHost, } from '@symbiote-native/components/bootstrap';
7
7
  import { AppRegistry, setHostRegistrar } from './modules/app-registry/index.js';
8
8
  // Mirrors the Vue/React adapters' createApp(App).mount(appName) two-step idiom.
9
9
  export function createApp(RootComponent, options = {}) {
@@ -25,7 +25,7 @@
25
25
  // compile error at the call site rather than a surprise at runtime. An empty object is
26
26
  // deliberately NOT treated as a clsx map: both readings contribute no style, so the cheaper,
27
27
  // non-allocating branch is the right one.
28
- import { isClassNameValue, resolveClassName } from '@symbiote-native/engine';
28
+ import { isClassNameValue, resolveClassName, } from '@symbiote-native/engine';
29
29
  // Returns a class STRING when the value is clsx-shaped, and the value UNTOUCHED otherwise, so
30
30
  // everything the engine already resolved correctly keeps taking the exact path it took before.
31
31
  export function normalizeSvelteClass(value) {
@@ -20,8 +20,10 @@
20
20
 
21
21
  dlog('RefreshControl -> PullToRefreshView');
22
22
  dlog(`RefreshControl refreshing=${String(rest.refreshing)}`);
23
- if (rest.enabled !== undefined) dlog(`RefreshControl enabled=${String(rest.enabled)} (Android-only)`);
24
- if (rest.onRefresh !== undefined) dlog('RefreshControl onRefresh listener wired');
23
+ if (rest.enabled !== undefined)
24
+ dlog(`RefreshControl enabled=${String(rest.enabled)} (Android-only)`);
25
+ if (rest.onRefresh !== undefined)
26
+ dlog('RefreshControl onRefresh listener wired');
25
27
 
26
28
  const bag = $derived(resolveAccessibilityProps(rest));
27
29
 
@@ -7,17 +7,26 @@
7
7
  </script>
8
8
 
9
9
  <script lang="ts">
10
+ import { resolveTextProps } from '@symbiote-native/components';
10
11
  import { createAttachmentsSync } from '../runes/attachments';
11
12
  import type { ShimElement } from '../dom-shim';
12
13
 
13
- let { children, ...bag }: ITextProps = $props();
14
+ let { children, ...rest }: ITextProps = $props();
14
15
 
15
16
  // See View.svelte's note: `{@attach}` arrives as a symbol-keyed entry in the same rest object.
16
17
  let hostShim = $state.raw<ShimElement | null>(null);
17
18
  const syncAttachments = createAttachmentsSync();
18
19
  $effect(() => {
19
- syncAttachments(hostShim, bag);
20
+ syncAttachments(hostShim, rest);
20
21
  });
22
+
23
+ // A FRESH object per pass, exactly as View.svelte builds its own: ShimElement's `p` setter
24
+ // diffs the incoming bag against the one it stored last time, so handing it the SAME live rest
25
+ // proxy twice compares that object with itself and applies nothing — every prop update after
26
+ // mount was silently dropped.
27
+ // resolveTextProps carries RN's Text.js defaults (ellipsizeMode 'tail', allowFontScaling
28
+ // true); without them native falls back to `clip` and a clamped Text cuts mid-word.
29
+ const bag = $derived(resolveTextProps({ ...rest }));
21
30
  </script>
22
31
 
23
32
  <symbiote-text p={bag} bind:this={hostShim}>
@@ -33,7 +33,9 @@
33
33
  // the raw `id` key so it never reaches Fabric (every non-function prop in the bag otherwise
34
34
  // passes through to the slot as-is) — mirrors React's `resolveId` in
35
35
  // `adapters/react/src/components.ts`, adapted to the object-bag idiom.
36
- const bag = $derived(id === undefined ? { ...rest, nativeID } : { ...rest, nativeID: id });
36
+ const bag = $derived(
37
+ id === undefined ? { ...rest, nativeID } : { ...rest, nativeID: id },
38
+ );
37
39
  </script>
38
40
 
39
41
  <symbiote-view p={bag} bind:this={hostShim}>
@@ -12,7 +12,10 @@
12
12
  </script>
13
13
 
14
14
  <script lang="ts">
15
- import { renderActivityIndicator, resolveAccessibilityProps } from '@symbiote-native/components';
15
+ import {
16
+ renderActivityIndicator,
17
+ resolveAccessibilityProps,
18
+ } from '@symbiote-native/components';
16
19
  import { PLATFORM } from './activity-indicator-platform';
17
20
  import { createDescriptorChildrenSync } from '../../descriptor-to-svelte';
18
21
  import { createAttachmentsSync } from '../../runes/attachments';
@@ -36,7 +39,10 @@
36
39
  size,
37
40
  color,
38
41
  style,
39
- passthrough: { ...resolveAccessibilityProps(passthrough), class: className },
42
+ passthrough: {
43
+ ...resolveAccessibilityProps(passthrough),
44
+ class: className,
45
+ },
40
46
  },
41
47
  PLATFORM,
42
48
  ),
@@ -11,7 +11,10 @@
11
11
  </script>
12
12
 
13
13
  <script lang="ts">
14
- import { BUTTON_ACCESSIBILITY_ROLE, resolveButtonTextStyle } from '@symbiote-native/components';
14
+ import {
15
+ BUTTON_ACCESSIBILITY_ROLE,
16
+ resolveButtonTextStyle,
17
+ } from '@symbiote-native/components';
15
18
  import TouchableOpacity from './touchable-opacity/index.svelte';
16
19
 
17
20
  let {
@@ -38,9 +41,11 @@
38
41
  // excess-property typing does not reject the native-only keys. TV-focus is inert on a phone.
39
42
  const nativeForward = $derived.by(() => {
40
43
  const forward: Record<string, unknown> = { testID };
41
- if (hasTVPreferredFocus !== undefined) forward.hasTVPreferredFocus = hasTVPreferredFocus;
44
+ if (hasTVPreferredFocus !== undefined)
45
+ forward.hasTVPreferredFocus = hasTVPreferredFocus;
42
46
  if (nextFocusDown !== undefined) forward.nextFocusDown = nextFocusDown;
43
- if (nextFocusForward !== undefined) forward.nextFocusForward = nextFocusForward;
47
+ if (nextFocusForward !== undefined)
48
+ forward.nextFocusForward = nextFocusForward;
44
49
  if (nextFocusLeft !== undefined) forward.nextFocusLeft = nextFocusLeft;
45
50
  if (nextFocusRight !== undefined) forward.nextFocusRight = nextFocusRight;
46
51
  if (nextFocusUp !== undefined) forward.nextFocusUp = nextFocusUp;
@@ -23,10 +23,16 @@
23
23
  type ISeparators,
24
24
  type IViewableItemsChangedInfo,
25
25
  } from '@symbiote-native/components';
26
- import { resolveClassName, type ISymbioteNode } from '@symbiote-native/engine';
26
+ import {
27
+ resolveClassName,
28
+ type ISymbioteNode,
29
+ } from '@symbiote-native/engine';
27
30
  import VirtualizedList from '../virtualized-list/index.svelte';
28
31
  import { pickAccessibilityProps } from '../virtualized-list/virtualized-list-props';
29
- import type { IVirtualizedListHandle, IScrollViewHandle } from '../virtualized-list/virtualized-list-props';
32
+ import type {
33
+ IVirtualizedListHandle,
34
+ IScrollViewHandle,
35
+ } from '../virtualized-list/virtualized-list-props';
30
36
  import type { IFlatListProps as IProps } from './flat-list-props';
31
37
  import { pickAttachmentProps } from '../../runes/attachments';
32
38
 
@@ -37,7 +43,9 @@
37
43
  // scrollHandle over its ShimElement).
38
44
  let inner = $state.raw<IVirtualizedListHandle | null>(null);
39
45
 
40
- const numColumns = $derived(typeof props.numColumns === 'number' ? props.numColumns : SINGLE_COLUMN);
46
+ const numColumns = $derived(
47
+ typeof props.numColumns === 'number' ? props.numColumns : SINGLE_COLUMN,
48
+ );
41
49
  const rows = $derived.by((): IRow<ItemT>[] =>
42
50
  numColumns > SINGLE_COLUMN ? chunkIntoRows(props.data, numColumns) : [],
43
51
  );
@@ -56,7 +64,9 @@
56
64
  }
57
65
 
58
66
  function isStyleLike(value: unknown): boolean {
59
- return Array.isArray(value) || (typeof value === 'object' && value !== null);
67
+ return (
68
+ Array.isArray(value) || (typeof value === 'object' && value !== null)
69
+ );
60
70
  }
61
71
 
62
72
  const rowStyle = $derived.by(() => [
@@ -70,7 +80,9 @@
70
80
 
71
81
  // The divider between rows shows real items (last of the row above, first of the row below), so
72
82
  // the user's `separator` snippet, typed on ItemT, sees items rather than the IRow wrapper.
73
- function rowSeparatorProps(entryProps: ISeparatorProps<IRow<ItemT>>): ISeparatorProps<ItemT> {
83
+ function rowSeparatorProps(
84
+ entryProps: ISeparatorProps<IRow<ItemT>>,
85
+ ): ISeparatorProps<ItemT> {
74
86
  return {
75
87
  ...entryProps,
76
88
  leadingItem: lastItemOfRow(entryProps.leadingItem),
@@ -87,8 +99,12 @@
87
99
  const rowViewabilityPairs = $derived(
88
100
  props.viewabilityConfigCallbackPairs?.map(pair => ({
89
101
  viewabilityConfig: pair.viewabilityConfig,
90
- onViewableItemsChanged: (rowInfo: IViewableItemsChangedInfo<IRow<ItemT>>): void => {
91
- pair.onViewableItemsChanged(expandRowViewability(rowInfo, props.keyExtractor));
102
+ onViewableItemsChanged: (
103
+ rowInfo: IViewableItemsChangedInfo<IRow<ItemT>>,
104
+ ): void => {
105
+ pair.onViewableItemsChanged(
106
+ expandRowViewability(rowInfo, props.keyExtractor),
107
+ );
92
108
  },
93
109
  })),
94
110
  );
@@ -100,7 +116,10 @@
100
116
  const accessibilityProps = $derived(pickAccessibilityProps(props));
101
117
 
102
118
  // ---- imperative handle: delegate straight through to the inner VirtualizedList instance. ----
103
- export function scrollToOffset(params: { offset: number; animated?: boolean }): void {
119
+ export function scrollToOffset(params: {
120
+ offset: number;
121
+ animated?: boolean;
122
+ }): void {
104
123
  inner?.scrollToOffset(params);
105
124
  }
106
125
  export function scrollToIndex(params: {
@@ -111,7 +130,11 @@
111
130
  }): void {
112
131
  inner?.scrollToIndex(params);
113
132
  }
114
- export function scrollToItem(params: { item: unknown; animated?: boolean; viewPosition?: number }): void {
133
+ export function scrollToItem(params: {
134
+ item: unknown;
135
+ animated?: boolean;
136
+ viewPosition?: number;
137
+ }): void {
115
138
  inner?.scrollToItem(params);
116
139
  }
117
140
  export function scrollToEnd(params?: { animated?: boolean }): void {
@@ -141,11 +164,23 @@
141
164
  const attachments = $derived(pickAttachmentProps(props));
142
165
  </script>
143
166
 
144
- {#snippet rowItem({ item: row, index, separators }: { item: IRow<ItemT>; index: number; separators: ISeparators })}
167
+ {#snippet rowItem({
168
+ item: row,
169
+ index,
170
+ separators,
171
+ }: {
172
+ item: IRow<ItemT>;
173
+ index: number;
174
+ separators: ISeparators;
175
+ })}
145
176
  <symbiote-view p={{ style: rowStyle }}>
146
177
  {#each row.items as rowItem, column (props.keyExtractor ? props.keyExtractor(rowItem, row.startIndex + column) : String(row.startIndex + column))}
147
178
  <symbiote-view p={{ style: { flex: 1 } }}>
148
- {@render props.item({ item: rowItem, index: row.startIndex + column, separators })}
179
+ {@render props.item({
180
+ item: rowItem,
181
+ index: row.startIndex + column,
182
+ separators,
183
+ })}
149
184
  </symbiote-view>
150
185
  {/each}
151
186
  </symbiote-view>
@@ -42,7 +42,13 @@ function expandSrcSet(srcSet, view, headers) {
42
42
  continue;
43
43
  if (scale === 1)
44
44
  useSrcForDefaultScale = false;
45
- sources.push({ uri, scale, width: view.width, height: view.height, ...{ headers } });
45
+ sources.push({
46
+ uri,
47
+ scale,
48
+ width: view.width,
49
+ height: view.height,
50
+ ...{ headers },
51
+ });
46
52
  }
47
53
  if (useSrcForDefaultScale && view.src !== undefined) {
48
54
  sources.push({
@@ -63,7 +69,9 @@ function resolveSourceArray(view) {
63
69
  return expandSrcSet(view.srcSet, view, headers);
64
70
  }
65
71
  if (view.src !== undefined) {
66
- return [{ uri: view.src, width: view.width, height: view.height, ...{ headers } }];
72
+ return [
73
+ { uri: view.src, width: view.width, height: view.height, ...{ headers } },
74
+ ];
67
75
  }
68
76
  if (view.source === undefined) {
69
77
  dlog('Image: no source / src / srcSet provided');
@@ -72,7 +80,9 @@ function resolveSourceArray(view) {
72
80
  const sources = normalizeSource(view.source);
73
81
  if (Object.keys(headers).length > 0 && sources.length === 1) {
74
82
  const [only] = sources;
75
- if (typeof only === 'object' && only !== null && typeof Reflect.get(only, 'uri') === 'string') {
83
+ if (typeof only === 'object' &&
84
+ only !== null &&
85
+ typeof Reflect.get(only, 'uri') === 'string') {
76
86
  return [{ ...only, headers }];
77
87
  }
78
88
  }
@@ -13,7 +13,10 @@
13
13
  </script>
14
14
 
15
15
  <script lang="ts">
16
- import { renderImageBackground, resolveAccessibilityProps } from '@symbiote-native/components';
16
+ import {
17
+ renderImageBackground,
18
+ resolveAccessibilityProps,
19
+ } from '@symbiote-native/components';
17
20
  import { resolveClassName } from '@symbiote-native/engine';
18
21
  import { createAttachmentsSync } from '../../runes/attachments';
19
22
  import type { ShimElement } from '../../dom-shim';
@@ -78,4 +81,7 @@
78
81
  });
79
82
  </script>
80
83
 
81
- <symbiote-view p={wrapperBag} bind:this={hostShim}><symbiote-image p={imageBag} />{@render children?.()}</symbiote-view>
84
+ <symbiote-view p={wrapperBag} bind:this={hostShim}>
85
+ <symbiote-image p={imageBag} />
86
+ {@render children?.()}
87
+ </symbiote-view>
@@ -43,8 +43,8 @@ export type { IStickyHeaderComponentProps } from './scroll-view/sticky-header-pr
43
43
  export { default as VirtualizedList } from './virtualized-list/index.svelte';
44
44
  export type { IVirtualizedListProps, IVirtualizedListHandle, } from './virtualized-list/virtualized-list-props';
45
45
  export { default as FlatList } from './flat-list/index.svelte';
46
- export type { IFlatListProps, IFlatListHandle } from './flat-list/flat-list-props';
46
+ export type { IFlatListProps, IFlatListHandle, } from './flat-list/flat-list-props';
47
47
  export { default as VirtualizedSectionList } from './virtualized-section-list/index.svelte';
48
48
  export type { IVirtualizedSectionListProps } from './virtualized-section-list/virtualized-section-list-props';
49
49
  export { default as SectionList } from './section-list/index.svelte';
50
- export type { ISectionListProps, ISection } from './section-list/section-list-props';
50
+ export type { ISectionListProps, ISection, } from './section-list/section-list-props';
@@ -14,7 +14,10 @@
14
14
  </script>
15
15
 
16
16
  <script lang="ts">
17
- import { renderInputAccessoryView, resolveAccessibilityProps } from '@symbiote-native/components';
17
+ import {
18
+ renderInputAccessoryView,
19
+ resolveAccessibilityProps,
20
+ } from '@symbiote-native/components';
18
21
  import { createAttachmentsSync } from '../../runes/attachments';
19
22
  import type { ShimElement } from '../../dom-shim';
20
23
 
@@ -5,8 +5,8 @@
5
5
  // this adapter supplies only the lifecycle: $state for the inset, a mutable frame/initialHeight
6
6
  // pair (NOT $state — changing them alone shouldn't re-render, same non-reactive-ref shape as
7
7
  // React's frameRef/initialHeightRef and Vue's plain `let frame`), an $effect subscribing to the
8
- // Keyboard module (mirrors React's useEffect / Vue's onMounted+onUnmounted pair), and the
9
- // wrapper assembly around the Snippet children.
8
+ // Keyboard module and reading the cross-fade device setting once (mirrors React's useEffect /
9
+ // Vue's onMounted+onUnmounted pair), and the wrapper assembly around the Snippet children.
10
10
  //
11
11
  // render-keyboard-avoiding-view.ts does NOT return a Descriptor here (by its own design — it
12
12
  // returns a layout DESCRIPTION instead, because KAV wraps arbitrary user children the Descriptor
@@ -21,7 +21,7 @@
21
21
  <script lang="ts">
22
22
  import {
23
23
  Keyboard,
24
- KEYBOARD_EVENT,
24
+ Platform,
25
25
  dlog,
26
26
  type IEventSubscription,
27
27
  type ISymbioteEvent,
@@ -30,6 +30,8 @@
30
30
  import type { ShimElement } from '../../dom-shim';
31
31
  import {
32
32
  computeInset,
33
+ keyboardAvoidingEventNamesFor,
34
+ readPrefersCrossFadeTransitions,
33
35
  readKeyboardFrame,
34
36
  readLayoutFrame,
35
37
  resolveKeyboardAvoidingLayout,
@@ -55,10 +57,22 @@
55
57
  // next keyboard event's inset math.
56
58
  let frame: IMeasuredFrame | undefined;
57
59
  let initialHeight: number | undefined;
60
+ // A device accessibility setting, not component state: it cannot change mid-session, and nothing
61
+ // in the markup reads it — it only feeds the next keyboard event's math — so a plain `let`, not
62
+ // $state. Resolves false on Android and when the native getter is absent.
63
+ let prefersCrossFadeTransitions = false;
58
64
 
59
65
  function onShow(payload: unknown): void {
60
66
  const keyboard = readKeyboardFrame(payload);
61
- const next = computeInset(frame, keyboard, keyboardVerticalOffset);
67
+ // `behavior` and `inset` are both read HERE, at event time, never captured when this handler
68
+ // was built: Svelte compiles a destructured prop into a live getter, so a `behavior` changed
69
+ // after mount reaches the math on the next event, and 'height' mode feeds back the inset
70
+ // CURRENTLY applied (RN's this.state.bottom) to cancel the shrink its own wrapper caused.
71
+ const next = computeInset(frame, keyboard, keyboardVerticalOffset, {
72
+ behavior,
73
+ previousInset: inset,
74
+ prefersCrossFadeTransitions,
75
+ });
62
76
  dlog(`KeyboardAvoidingView show -> inset ${next}`);
63
77
  inset = next;
64
78
  }
@@ -68,15 +82,22 @@
68
82
  }
69
83
 
70
84
  // No reactive state is read synchronously in this callback body (only inside onShow/onHide,
71
- // which run later at event-time via closures over the live `keyboardVerticalOffset` prop), so
72
- // this effect has zero tracked dependencies and runs exactly once per mount/unmount — the same
73
- // subscribe-once/cleanup-once shape as Vue's onMounted+onUnmounted pair.
85
+ // which run later at event-time via closures over the live props and `inset`), so this effect has
86
+ // zero tracked dependencies and runs exactly once per mount/unmount — the same subscribe-once/
87
+ // cleanup-once shape as Vue's onMounted+onUnmounted pair. TWO listeners, never three: RN's own
88
+ // comment warns that a change-frame notification arrives BEFORE the hide one on an undocked iOS
89
+ // keyboard, so subscribing to it applies a frame captured mid-dismissal.
74
90
  $effect(() => {
91
+ const events = keyboardAvoidingEventNamesFor(Platform.OS);
75
92
  const subscriptions: IEventSubscription[] = [
76
- Keyboard.addListener(KEYBOARD_EVENT.didShow, onShow),
77
- Keyboard.addListener(KEYBOARD_EVENT.didChangeFrame, onShow),
78
- Keyboard.addListener(KEYBOARD_EVENT.didHide, onHide),
93
+ Keyboard.addListener(events.show, onShow),
94
+ Keyboard.addListener(events.hide, onHide),
79
95
  ];
96
+ // Through the core wrapper, not AccessibilityInfo directly: nobody awaits this, and the
97
+ // engine's iOS getters reject on a native error, so the fallback-to-false lives there once.
98
+ void readPrefersCrossFadeTransitions().then(prefers => {
99
+ prefersCrossFadeTransitions = prefers;
100
+ });
80
101
  return () => {
81
102
  for (const subscription of subscriptions) subscription.remove();
82
103
  };
@@ -111,7 +132,9 @@
111
132
  onLayout: handleLayout,
112
133
  });
113
134
 
114
- const innerBag = $derived(layout.kind === 'nested' ? { style: layout.innerStyle } : undefined);
135
+ const innerBag = $derived(
136
+ layout.kind === 'nested' ? { style: layout.innerStyle } : undefined,
137
+ );
115
138
 
116
139
  // See View.svelte's note on `{@attach}` — bound to the OUTER wrapper, which is the node
117
140
  // this component's own style lands on in both the nested and flat layouts.
@@ -53,7 +53,10 @@
53
53
  // isVisible=false with isRendered still true before this effect flips it) but isn't observable
54
54
  // as its own committed Fabric frame the way React's per-render synchronous commit makes it.
55
55
  $effect(() => {
56
- state = modalReducer(state, isVisible ? { type: 'show' } : { type: 'hide' });
56
+ state = modalReducer(
57
+ state,
58
+ isVisible ? { type: 'show' } : { type: 'hide' },
59
+ );
57
60
  });
58
61
 
59
62
  $effect(() => {
@@ -124,14 +124,21 @@
124
124
  // other Svelte component that hand-authors a raw host tag (RefreshControl.svelte, modal/
125
125
  // index.svelte).
126
126
  const resolved = $derived(
127
- resolveAccessibilityProps({ ...rest, accessibilityState: resolvedAccessibilityState }),
127
+ resolveAccessibilityProps({
128
+ ...rest,
129
+ accessibilityState: resolvedAccessibilityState,
130
+ }),
128
131
  );
129
132
 
130
- const resolvedStyle = $derived(typeof style === 'function' ? style(state) : style);
133
+ const resolvedStyle = $derived(
134
+ typeof style === 'function' ? style(state) : style,
135
+ );
131
136
 
132
137
  // android_ripple rides a dedicated inner View; on iOS rippleProps() returns undefined, so the
133
138
  // child renders unwrapped, no extra node — mirrors React's Pressable + touchable-native-feedback.
134
- const ripple = $derived(android_ripple !== undefined ? rippleProps(android_ripple) : undefined);
139
+ const ripple = $derived(
140
+ android_ripple !== undefined ? rippleProps(android_ripple) : undefined,
141
+ );
135
142
 
136
143
  const bag = $derived({
137
144
  ...resolved,
@@ -2,7 +2,7 @@ import type { Snippet } from 'svelte';
2
2
  import type { IStyleProp, IViewStyle } from '@symbiote-native/engine';
3
3
  import type { IAccessibilityProps, IAriaProps, IPressHandler, IPressState, IRectOffset, IPressableAndroidRippleConfig } from '@symbiote-native/components';
4
4
  import type { ISvelteClassValue } from '../../class-value';
5
- export type { IPressState, IPressableAndroidRippleConfig } from '@symbiote-native/components';
5
+ export type { IPressState, IPressableAndroidRippleConfig, } from '@symbiote-native/components';
6
6
  export interface IPressableProps extends IAccessibilityProps, IAriaProps {
7
7
  onPress?: IPressHandler;
8
8
  onPressIn?: IPressHandler;
@@ -85,7 +85,8 @@
85
85
  }: IScrollViewProps = $props();
86
86
 
87
87
  $effect(() => {
88
- if (stickyHeaderIndices === undefined || stickyHeaderIndices.length === 0) return;
88
+ if (stickyHeaderIndices === undefined || stickyHeaderIndices.length === 0)
89
+ return;
89
90
  dlog(
90
91
  'ScrollView.stickyHeaderIndices is not auto-honored on Svelte (no index-wrap mechanism — ' +
91
92
  'see scroll-view-props.ts KNOWN GAP); compose ScrollViewStickyHeader manually instead',
@@ -103,7 +104,11 @@
103
104
  // Plain exported functions on the instance script are what a parent's `bind:this={ref}` sees —
104
105
  // the Svelte mechanism for exposing an imperative handle, the twin of React's
105
106
  // useImperativeHandle / Vue's expose().
106
- export function scrollTo(options?: { x?: number; y?: number; animated?: boolean }): void {
107
+ export function scrollTo(options?: {
108
+ x?: number;
109
+ y?: number;
110
+ animated?: boolean;
111
+ }): void {
107
112
  handle.scrollTo(options);
108
113
  }
109
114
  export function scrollToEnd(options?: { animated?: boolean }): void {
@@ -117,7 +122,9 @@
117
122
  }
118
123
 
119
124
  const isHorizontal = $derived(horizontal === true);
120
- const hasStickyHeaders = $derived(stickyHeaderIndices !== undefined && stickyHeaderIndices.length > 0);
125
+ const hasStickyHeaders = $derived(
126
+ stickyHeaderIndices !== undefined && stickyHeaderIndices.length > 0,
127
+ );
121
128
  const shouldWrapRefreshControl = $derived(
122
129
  PLATFORM.refreshControlMode === 'wrap' && refreshControl !== undefined,
123
130
  );
@@ -131,7 +138,9 @@
131
138
  // style and collapse it to nothing — e.g. App.svelte's `class="screen"` (flex:1) on the
132
139
  // top-level ScrollView, which left the wrapper with no height for its content to grow into.
133
140
  const layoutSplit = $derived(
134
- shouldWrapRefreshControl ? splitLayoutProps([resolveSvelteClass(className), style]) : undefined,
141
+ shouldWrapRefreshControl
142
+ ? splitLayoutProps([resolveSvelteClass(className), style])
143
+ : undefined,
135
144
  );
136
145
 
137
146
  // A single AnimatedValue tracks the scroll offset (RN's _scrollAnimatedValue), allocated once
@@ -161,7 +170,9 @@
161
170
  // it solely `if (isFabric)`). Forcing the JS path to keep that listener alive only gives up the
162
171
  // native driver, putting the pin on the JS thread — drift on iOS, outright failure on Android
163
172
  // (commit debounce 15ms vs iOS's 64ms — render-scroll-sticky.ts).
164
- const nativeStickyAvailable = $derived(hasStickyHeaders && isNativeAnimatedAvailable());
173
+ const nativeStickyAvailable = $derived(
174
+ hasStickyHeaders && isNativeAnimatedAvailable(),
175
+ );
165
176
 
166
177
  // Native sticky-scroll attach (RN attachNativeEvent / _updateAnimatedNodeAttachment) — NOT used
167
178
  // for sticky headers (see the comment above); kept for other native-event-attach consumers.
@@ -178,7 +189,9 @@
178
189
  : contentContainerStyle,
179
190
  );
180
191
 
181
- const intrinsics = $derived(selectScrollIntrinsics(isHorizontal, resolvedContentContainerStyle));
192
+ const intrinsics = $derived(
193
+ selectScrollIntrinsics(isHorizontal, resolvedContentContainerStyle),
194
+ );
182
195
 
183
196
  const forwarding = $derived(
184
197
  resolveScrollForwarding({
@@ -186,7 +199,8 @@
186
199
  nativeStickyAvailable,
187
200
  invertStickyHeaders,
188
201
  scrollEventThrottle,
189
- maintainVisibleContentPosition: passthrough.maintainVisibleContentPosition,
202
+ maintainVisibleContentPosition:
203
+ passthrough.maintainVisibleContentPosition,
190
204
  snapToAlignment: passthrough.snapToAlignment,
191
205
  }),
192
206
  );
@@ -220,7 +234,10 @@
220
234
  [{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }],
221
235
  onScroll === undefined
222
236
  ? undefined
223
- : { listener: (...args: unknown[]) => forwardScrollEvent(onScroll, args) },
237
+ : {
238
+ listener: (...args: unknown[]) =>
239
+ forwardScrollEvent(onScroll, args),
240
+ },
224
241
  );
225
242
  });
226
243
 
@@ -243,7 +260,9 @@
243
260
  // layoutSplit already folded className's resolved value into inner/outer above — forwarding
244
261
  // the raw class here too would re-apply its LAYOUT half onto the inner scroll view a second
245
262
  // time (on top of the wrapper).
246
- ...(className !== undefined && layoutSplit === undefined ? { class: className } : {}),
263
+ ...(className !== undefined && layoutSplit === undefined
264
+ ? { class: className }
265
+ : {}),
247
266
  style: scrollStyle,
248
267
  onScroll: resolvedScrollHandler,
249
268
  onLayout: handleScrollLayout,
@@ -260,7 +279,9 @@
260
279
  style: intrinsics.contentStyle,
261
280
  collapsable: false,
262
281
  ...(forwarding.collapsableChildren ? { collapsableChildren: false } : {}),
263
- ...(onContentSizeChange !== undefined ? { onLayout: handleContentLayout } : {}),
282
+ ...(onContentSizeChange !== undefined
283
+ ? { onLayout: handleContentLayout }
284
+ : {}),
264
285
  }));
265
286
  // See View.svelte's note on `{@attach}`.
266
287
  const syncAttachments = createAttachmentsSync();
@@ -270,29 +291,41 @@
270
291
  </script>
271
292
 
272
293
  {#snippet scrollBody()}
273
- <!-- Packed edge-to-edge against the next block deliberately (svelte-adapter-dom-shim §16):
274
- whitespace BETWEEN two sibling blocks survives clean_nodes as a single-space text node,
275
- which here becomes a real RCTRawText child of a scroll view — an invalid Fabric child that
276
- fails silently. Do not reformat this join. -->
294
+ <!-- The gap between these two sibling blocks survives clean_nodes as a ' ' text node, but
295
+ the shim drops a whitespace-only node whose parent takes no raw text, so it never
296
+ reaches Fabric (dom-shim/text.ts, svelte-adapter-dom-shim §16b). -->
277
297
  {#if !shouldWrapRefreshControl && refreshControl !== undefined}
278
298
  <RefreshControl {...refreshControl} />
279
- {/if}{#if isHorizontal}
280
- <symbiote-horizontal-scroll-content p={contentBag}>{@render children?.()}</symbiote-horizontal-scroll-content>
299
+ {/if}
300
+ {#if isHorizontal}
301
+ <symbiote-horizontal-scroll-content p={contentBag}>
302
+ {@render children?.()}
303
+ </symbiote-horizontal-scroll-content>
281
304
  {:else}
282
- <symbiote-scroll-content p={contentBag}>{@render children?.()}</symbiote-scroll-content>
305
+ <symbiote-scroll-content p={contentBag}>
306
+ {@render children?.()}
307
+ </symbiote-scroll-content>
283
308
  {/if}
284
309
  {/snippet}
285
310
 
286
311
  {#if shouldWrapRefreshControl && refreshControl !== undefined}
287
312
  <RefreshControl {...refreshControl} style={layoutSplit?.outer}>
288
313
  {#if isHorizontal}
289
- <symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>{@render scrollBody()}</symbiote-horizontal-scroll-view>
314
+ <symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>
315
+ {@render scrollBody()}
316
+ </symbiote-horizontal-scroll-view>
290
317
  {:else}
291
- <symbiote-scroll-view p={outerBag} bind:this={hostShim}>{@render scrollBody()}</symbiote-scroll-view>
318
+ <symbiote-scroll-view p={outerBag} bind:this={hostShim}>
319
+ {@render scrollBody()}
320
+ </symbiote-scroll-view>
292
321
  {/if}
293
322
  </RefreshControl>
294
323
  {:else if isHorizontal}
295
- <symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>{@render scrollBody()}</symbiote-horizontal-scroll-view>
324
+ <symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>
325
+ {@render scrollBody()}
326
+ </symbiote-horizontal-scroll-view>
296
327
  {:else}
297
- <symbiote-scroll-view p={outerBag} bind:this={hostShim}>{@render scrollBody()}</symbiote-scroll-view>
328
+ <symbiote-scroll-view p={outerBag} bind:this={hostShim}>
329
+ {@render scrollBody()}
330
+ </symbiote-scroll-view>
298
331
  {/if}