@symbiote-native/svelte 0.2.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) 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 +48 -23
  16. package/build/components/pressable/index.svelte.d.ts +5 -1
  17. package/build/components/pressable/pressable-props.d.ts +1 -1
  18. package/build/components/safe-area-view-props.d.ts +1 -0
  19. package/build/components/scroll-view/index.svelte +54 -21
  20. package/build/components/scroll-view/sticky-header.svelte +49 -16
  21. package/build/components/section-list/index.svelte +6 -1
  22. package/build/components/switch/index.svelte +16 -5
  23. package/build/components/text-input/index.svelte +80 -8
  24. package/build/components/text-input/index.svelte.d.ts +5 -0
  25. package/build/components/touchable-highlight/index.svelte +115 -15
  26. package/build/components/touchable-highlight/touchable-highlight-props.d.ts +2 -0
  27. package/build/components/touchable-native-feedback/touchable-native-feedback.svelte +8 -3
  28. package/build/components/touchable-opacity/index.svelte +113 -56
  29. package/build/components/touchable-without-feedback/index.svelte +60 -4
  30. package/build/components/virtualized-list/index.svelte +233 -70
  31. package/build/components/virtualized-list/virtualized-list-props.js +8 -4
  32. package/build/components/virtualized-section-list/index.svelte +56 -9
  33. package/build/components/virtualized-section-list/index.svelte.d.ts +1 -1
  34. package/build/components/virtualized-section-list/virtualized-section-list-props.d.ts +5 -0
  35. package/build/create-portal/index.d.ts +22 -0
  36. package/build/create-portal/index.js +160 -0
  37. package/build/descriptor-to-svelte.js +15 -21
  38. package/build/dom-shim/element.d.ts +9 -3
  39. package/build/dom-shim/element.js +77 -25
  40. package/build/dom-shim/fold-host-bag.d.ts +1 -0
  41. package/build/dom-shim/fold-host-bag.js +7 -0
  42. package/build/dom-shim/index.d.ts +1 -0
  43. package/build/dom-shim/index.js +3 -0
  44. package/build/dom-shim/patch-globals.js +4 -2
  45. package/build/dom-shim/shim-node.js +11 -0
  46. package/build/dom-shim/text.js +38 -1
  47. package/build/host-instance.js +9 -5
  48. package/build/index.d.ts +6 -3
  49. package/build/index.js +20 -11
  50. package/build/modules/animated/create-animated-component.d.ts +5 -0
  51. package/build/modules/animated/create-animated-component.js +131 -0
  52. package/build/modules/animated/index.d.ts +24 -14
  53. package/build/modules/animated/index.js +30 -30
  54. package/build/modules/status-bar/index.js +1 -1
  55. package/build/modules/status-bar/index.svelte +4 -1
  56. package/build/native-view-bridge.d.ts +1 -1
  57. package/build/native-view-bridge.js +1 -1
  58. package/build/preprocessor/collapse-text-whitespace.js +20 -7
  59. package/build/preprocessor/forbid-web-only-constructs.js +8 -2
  60. package/build/preprocessor/lower-host-primitives.d.ts +10 -0
  61. package/build/preprocessor/lower-host-primitives.js +455 -0
  62. package/build/preprocessor/scoped-styles.js +141 -104
  63. package/build/register.d.ts +1 -0
  64. package/build/register.js +33 -0
  65. package/build/render.js +48 -3
  66. package/build/scope-token.d.ts +3 -2
  67. package/build/scope-token.js +6 -17
  68. package/build/state-style.d.ts +1 -0
  69. package/build/state-style.js +8 -0
  70. package/build/style-scope.d.ts +2 -1
  71. package/build/style-scope.js +10 -8
  72. package/metro-svelte-transformer.cjs +48 -14
  73. package/package.json +28 -10
  74. package/build/modules/animated/AnimatedImage.svelte +0 -115
  75. package/build/modules/animated/AnimatedImage.svelte.d.ts +0 -5
  76. package/build/modules/animated/AnimatedScrollView.svelte +0 -92
  77. package/build/modules/animated/AnimatedScrollView.svelte.d.ts +0 -17
  78. package/build/modules/animated/AnimatedText.svelte +0 -58
  79. package/build/modules/animated/AnimatedText.svelte.d.ts +0 -5
  80. package/build/modules/animated/AnimatedView.svelte +0 -95
  81. 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(() => {
@@ -22,15 +22,18 @@
22
22
  </script>
23
23
 
24
24
  <script lang="ts">
25
+ import { onDestroy } from 'svelte';
25
26
  import {
26
27
  createPressHandlers,
27
28
  createPressRuntime,
29
+ disposePressRuntime,
28
30
  rippleProps,
29
31
  buildPressableListeners,
30
32
  resolveDisabledAccessibilityState,
31
33
  resolveAccessibilityProps,
32
34
  noteHoverNoop,
33
35
  DEFAULT_DELAY_LONG_PRESS_MS,
36
+ DEFAULT_MIN_PRESS_DURATION_MS,
34
37
  type IPressHost,
35
38
  type IPressState,
36
39
  } from '@symbiote-native/components';
@@ -50,6 +53,7 @@
50
53
  hitSlop,
51
54
  pressRetentionOffset,
52
55
  unstable_pressDelay = 0,
56
+ __minPressDuration = DEFAULT_MIN_PRESS_DURATION_MS,
53
57
  android_ripple,
54
58
  android_disableSound,
55
59
  onHoverIn,
@@ -61,7 +65,10 @@
61
65
  class: className,
62
66
  children,
63
67
  ...rest
64
- }: IPressableProps = $props();
68
+ }: IPressableProps & {
69
+ /** @internal Touchable* mirrors RN's Pressability minPressDuration: 0 override. */
70
+ __minPressDuration?: number;
71
+ } = $props();
65
72
 
66
73
  let pressed = $state(false);
67
74
  // Plain setup-scope object, never `$state`: mutated by the machine on every event, never
@@ -85,8 +92,13 @@
85
92
  const id = setTimeout(callback, ms);
86
93
  return () => clearTimeout(id);
87
94
  },
95
+ now: Date.now,
88
96
  };
89
97
 
98
+ onDestroy(() => {
99
+ disposePressRuntime(runtime);
100
+ });
101
+
90
102
  $effect(() => {
91
103
  noteHoverNoop(onHoverIn, onHoverOut);
92
104
  });
@@ -107,6 +119,7 @@
107
119
  onLongPress,
108
120
  delayLongPress,
109
121
  unstable_pressDelay,
122
+ minPressDuration: __minPressDuration,
110
123
  hitSlop,
111
124
  pressRetentionOffset,
112
125
  },
@@ -115,31 +128,43 @@
115
128
  ),
116
129
  );
117
130
 
118
- const resolvedAccessibilityState = $derived(
119
- resolveDisabledAccessibilityState(accessibilityState, disabled),
120
- );
121
-
122
- // Folds aria-*/role into accessibility* — Pressable owns its host node directly (it does not
123
- // compose View.svelte), so it must fold this itself, exactly like Vue's Pressable and every
124
- // other Svelte component that hand-authors a raw host tag (RefreshControl.svelte, modal/
125
- // index.svelte).
126
- const resolved = $derived(
127
- resolveAccessibilityProps({ ...rest, accessibilityState: resolvedAccessibilityState }),
128
- );
129
-
130
- const resolvedStyle = $derived(typeof style === 'function' ? style(state) : style);
131
-
132
131
  // android_ripple rides a dedicated inner View; on iOS rippleProps() returns undefined, so the
133
132
  // child renders unwrapped, no extra node — mirrors React's Pressable + touchable-native-feedback.
134
- const ripple = $derived(android_ripple !== undefined ? rippleProps(android_ripple) : undefined);
133
+ const ripple = $derived(
134
+ android_ripple !== undefined ? rippleProps(android_ripple) : undefined,
135
+ );
135
136
 
136
- const bag = $derived({
137
- ...resolved,
138
- style: resolvedStyle,
139
- class: className,
140
- hitSlop,
141
- ...(android_disableSound !== undefined ? { android_disableSound } : {}),
142
- ...buildPressableListeners(handlers, { disabled, cancelable }),
137
+ // ONE derived, not four. The accessibility fold and the style resolution used to be their own
138
+ // `$derived`s, each read by exactly this bag and by nothing else — a memo nobody can reuse, in
139
+ // exchange for a reaction-graph node per Pressable instance. A 1 000-row list mounts 2 000 of
140
+ // them, which made this component the largest single allocation site in a create profile.
141
+ // `handlers` and `ripple` stay separate BECAUSE they are read elsewhere: `handlers` must keep
142
+ // its identity across an unrelated bag change or every recompute re-registers native listeners,
143
+ // and `ripple` is read twice (the branch test and the inner view's own bag).
144
+ const bag = $derived.by(() => {
145
+ // Folds aria-*/role into accessibility* — Pressable owns its host node directly (it does not
146
+ // compose View.svelte), so it must fold this itself, exactly like Vue's Pressable and every
147
+ // other Svelte component that hand-authors a raw host tag (RefreshControl.svelte, modal/
148
+ // index.svelte).
149
+ const resolved = resolveAccessibilityProps({
150
+ ...rest,
151
+ accessibilityState: resolveDisabledAccessibilityState(
152
+ accessibilityState,
153
+ disabled,
154
+ ),
155
+ });
156
+ const next: Record<string, unknown> = {
157
+ ...resolved,
158
+ style: typeof style === 'function' ? style(state) : style,
159
+ class: className,
160
+ hitSlop,
161
+ ...buildPressableListeners(handlers, { disabled, cancelable }),
162
+ };
163
+ // Assigned rather than conditionally spread: `...(cond ? { x } : {})` allocated an empty
164
+ // object literal on every evaluation for the overwhelmingly common `undefined` case.
165
+ if (android_disableSound !== undefined)
166
+ next.android_disableSound = android_disableSound;
167
+ return next;
143
168
  });
144
169
 
145
170
  // See View.svelte's note on `{@attach}`.
@@ -1,5 +1,9 @@
1
1
  import type { IPressableProps } from './pressable-props';
2
2
  export type { IPressableProps };
3
- declare const Index: import("svelte").Component<IPressableProps, {}, "">;
3
+ type $$ComponentProps = IPressableProps & {
4
+ /** @internal Touchable* mirrors RN's Pressability minPressDuration: 0 override. */
5
+ __minPressDuration?: number;
6
+ };
7
+ declare const Index: import("svelte").Component<$$ComponentProps, {}, "">;
4
8
  type Index = ReturnType<typeof Index>;
5
9
  export default Index;
@@ -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;
@@ -4,6 +4,7 @@ import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/component
4
4
  import type { ISvelteClassValue } from '../class-value';
5
5
  export interface ISafeAreaViewProps extends IAccessibilityProps, IAriaProps {
6
6
  style?: IStyleProp<IViewStyle>;
7
+ id?: string;
7
8
  class?: ISvelteClassValue;
8
9
  onLayout?: (event: ISymbioteEvent) => void;
9
10
  children?: Snippet;