@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
@@ -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}
@@ -29,7 +29,8 @@
29
29
  //
30
30
  // Native-Animated-driven, like React's ScrollViewStickyHeader.js: the pinned translateY rides
31
31
  // as the raw AnimatedInterpolation node in style.transform on a real Animated.View
32
- // (AnimatedView.svelte), whose `__makeNative()` connects it straight to the native shadow tree,
32
+ // (createAnimatedComponent(View)), whose `__makeNative()` connects it straight to the native
33
+ // shadow tree,
33
34
  // so once native the pin tracks scroll on the UI thread with zero JS per frame - the
34
35
  // interpolation's own .addListener() below only drives the separate debounced
35
36
  // `passthroughAnimatedPropExplicitValues` (RN's own hit-testing sync).
@@ -51,9 +52,16 @@
51
52
  // why ScrollView/VirtualizedList force `nativeStickyAvailable = false`: the correct bootstrap
52
53
  // path, not a workaround.
53
54
  import type { IStickyHeaderComponentProps } from './sticky-header-props';
55
+ import View from '../View.svelte';
56
+ import { createAnimatedComponent } from '../../modules/animated/create-animated-component';
54
57
 
55
58
  export type { IStickyHeaderComponentProps };
56
59
 
60
+ // Wrapped here, not imported from modules/animated: that barrel imports ScrollView, which
61
+ // imports this file, so naming it would close a module cycle. Module scope, so every header
62
+ // instance shares one component identity.
63
+ const AnimatedView = createAnimatedComponent(View);
64
+
57
65
  // Diagnostic-only: a module-scope counter so every instance across every ScrollViewStickyHeader
58
66
  // in a device log is individually identifiable (see the mount/unmount dlog calls below).
59
67
  let stickyHeaderInstanceSeq = 0;
@@ -70,8 +78,13 @@
70
78
  type IStickyEffect,
71
79
  type IStickyReducerInputs,
72
80
  } from '@symbiote-native/components';
73
- import { AnimatedValue, Platform, dlog, type AnimatedInterpolation, type ISymbioteEvent } from '@symbiote-native/engine';
74
- import AnimatedView from '../../modules/animated/AnimatedView.svelte';
81
+ import {
82
+ AnimatedValue,
83
+ Platform,
84
+ dlog,
85
+ type AnimatedInterpolation,
86
+ type ISymbioteEvent,
87
+ } from '@symbiote-native/engine';
75
88
  import {
76
89
  SCROLL_VIEW_STICKY_CONTEXT_KEY,
77
90
  type IScrollViewStickyContext,
@@ -91,18 +104,26 @@
91
104
  // Resolve the scroll wiring from the parent ScrollView's context when not given explicitly.
92
105
  // A fresh standalone AnimatedValue is the fallback for a header used outside a ScrollView
93
106
  // (never sticks, but never crashes either).
94
- const stickyContext = getContext<IScrollViewStickyContext | undefined>(SCROLL_VIEW_STICKY_CONTEXT_KEY);
107
+ const stickyContext = getContext<IScrollViewStickyContext | undefined>(
108
+ SCROLL_VIEW_STICKY_CONTEXT_KEY,
109
+ );
95
110
  $effect(() => {
96
111
  if (stickyContext === undefined && scrollAnimatedValueProp === undefined) {
97
- dlog('ScrollViewStickyHeader used outside a ScrollView — sticky positioning is a no-op');
112
+ dlog(
113
+ 'ScrollViewStickyHeader used outside a ScrollView — sticky positioning is a no-op',
114
+ );
98
115
  }
99
116
  });
100
117
  // svelte-ignore state_referenced_locally -- captured ONCE by identity; it does not change
101
118
  // after mount, so re-deriving it would be wrong.
102
119
  const scrollAnimatedValue =
103
- scrollAnimatedValueProp ?? stickyContext?.scrollAnimatedValue ?? new AnimatedValue(0);
120
+ scrollAnimatedValueProp ??
121
+ stickyContext?.scrollAnimatedValue ??
122
+ new AnimatedValue(0);
104
123
  const inverted = $derived(invertedProp ?? stickyContext?.getInverted());
105
- const scrollViewHeight = $derived(scrollViewHeightProp ?? stickyContext?.getViewportHeight());
124
+ const scrollViewHeight = $derived(
125
+ scrollViewHeightProp ?? stickyContext?.getViewportHeight(),
126
+ );
106
127
 
107
128
  // The one folded state cell (RN's scattered useState/useRef collapsed into IStickyHeaderState),
108
129
  // mutated in place by reduceSticky. Named `stickyState`, not `state`: a local binding literally
@@ -115,7 +136,10 @@
115
136
  // The interpolation node currently bound into style.transform — held by IDENTITY ($state.raw),
116
137
  // rebuilt on every 'rebuild-interpolation' effect.
117
138
  let animatedTranslateY = $state.raw<AnimatedInterpolation>(
118
- scrollAnimatedValue.interpolate({ inputRange: [-1, 0], outputRange: [0, 0] }),
139
+ scrollAnimatedValue.interpolate({
140
+ inputRange: [-1, 0],
141
+ outputRange: [0, 0],
142
+ }),
119
143
  );
120
144
  let listenerId: string | undefined;
121
145
  let debounceTimer: ReturnType<typeof setTimeout> | undefined;
@@ -139,14 +163,18 @@
139
163
  // Detach the old listener, build a fresh interpolation onto the shared scroll value, and
140
164
  // wire the settled-value listener (drives ONLY the debounced hit-testing passthrough
141
165
  // below — the visible pin rides the native AnimatedProps connection in the markup).
142
- if (listenerId !== undefined) animatedTranslateY.removeListener(listenerId);
166
+ if (listenerId !== undefined)
167
+ animatedTranslateY.removeListener(listenerId);
143
168
  const next = scrollAnimatedValue.interpolate({
144
169
  inputRange: effect.inputRange,
145
170
  outputRange: effect.outputRange,
146
171
  });
147
- listenerId = next.addListener(({ value }: { value: number | string }): void => {
148
- if (typeof value === 'number') dispatch({ kind: 'animated-tick', value });
149
- });
172
+ listenerId = next.addListener(
173
+ ({ value }: { value: number | string }): void => {
174
+ if (typeof value === 'number')
175
+ dispatch({ kind: 'animated-tick', value });
176
+ },
177
+ );
150
178
  animatedTranslateY = next;
151
179
  break;
152
180
  }
@@ -160,7 +188,9 @@
160
188
  case 'apply-passthrough':
161
189
  // The actual committed translateY — a freeze at a huge/wrong number here is the
162
190
  // visible-disappearance symptom to watch for.
163
- dlog(`ScrollViewStickyHeader[y=${stickyState.layoutY}] apply-passthrough translateY=${effect.translateY}`);
191
+ dlog(
192
+ `ScrollViewStickyHeader[y=${stickyState.layoutY}] apply-passthrough translateY=${effect.translateY}`,
193
+ );
164
194
  version += 1;
165
195
  break;
166
196
  case 'record-header-y':
@@ -192,8 +222,11 @@
192
222
  // Detach the listener + clear the debounce on unmount.
193
223
  $effect(() => {
194
224
  return (): void => {
195
- dlog(`ScrollViewStickyHeader#${instanceId}[y=${stickyState.layoutY}] unmount`);
196
- if (listenerId !== undefined) animatedTranslateY.removeListener(listenerId);
225
+ dlog(
226
+ `ScrollViewStickyHeader#${instanceId}[y=${stickyState.layoutY}] unmount`,
227
+ );
228
+ if (listenerId !== undefined)
229
+ animatedTranslateY.removeListener(listenerId);
197
230
  if (debounceTimer !== undefined) clearTimeout(debounceTimer);
198
231
  };
199
232
  });
@@ -213,7 +246,7 @@
213
246
 
214
247
  // The EXPLICIT debounced translateY overrides the committed transform for hit-testing, while
215
248
  // `animatedTranslateY` (in the markup below) does the smooth native-driven pin — per RN
216
- // ScrollViewStickyHeader.js, and AnimatedView.svelte's own passthroughAnimatedPropExplicitValues
249
+ // ScrollViewStickyHeader.js, and createAnimatedComponent's own passthroughAnimatedPropExplicitValues
217
250
  // contract (readPassthroughStyle).
218
251
  const passthroughAnimatedPropExplicitValues = $derived.by(() => {
219
252
  void version;
@@ -6,7 +6,11 @@
6
6
  // (a Svelte `bind:this` on THIS component resolves to whatever THIS component exports, so the
7
7
  // wrapper must re-declare + delegate rather than re-exporting the child's exports directly) —
8
8
  // the Svelte twin of the React/Vue SectionList.
9
- import type { ISectionListProps, ISectionListHandle, ISection } from './section-list-props';
9
+ import type {
10
+ ISectionListProps,
11
+ ISectionListHandle,
12
+ ISection,
13
+ } from './section-list-props';
10
14
 
11
15
  export type { ISectionListProps, ISectionListHandle, ISection };
12
16
  </script>
@@ -77,6 +81,7 @@
77
81
  footer={props.footer}
78
82
  empty={props.empty}
79
83
  keyExtractor={props.keyExtractor}
84
+ getItemLayout={props.getItemLayout}
80
85
  stickySectionHeadersEnabled={props.stickySectionHeadersEnabled}
81
86
  inverted={props.inverted}
82
87
  extraData={props.extraData}
@@ -3,7 +3,7 @@
3
3
  // render-only). Reuses the shared logic verbatim (switchReducer / valueFromChange /
4
4
  // shouldSnapBack, exactly like React's useReducer + Vue's ref-based reducer wiring) and
5
5
  // calls renderSwitch() itself for the prop assembly — its Descriptor is always exactly one
6
- // `symbiote-switch` node with zero children (core/components/src/view/render-switch.ts). Root
6
+ // `symbiote-switch-managed` node with zero children (core/components/src/view/render-switch.ts). Root
7
7
  // stays a literal tag (bind:this needs a known tag); `createDescriptorChildrenSync` is still
8
8
  // wired for its (always-empty) children, matching every other category-1 component uniformly
9
9
  // — the same reason React still routes a childless Switch through descriptorToReact rather
@@ -27,7 +27,11 @@
27
27
  valueFromChange,
28
28
  renderSwitch,
29
29
  } from '@symbiote-native/components';
30
- import { dispatchViewCommand, dlog, type ISymbioteEvent } from '@symbiote-native/engine';
30
+ import {
31
+ dispatchViewCommand,
32
+ dlog,
33
+ type ISymbioteEvent,
34
+ } from '@symbiote-native/engine';
31
35
  import { PLATFORM } from './switch-platform';
32
36
  import { createDescriptorChildrenSync } from '../../descriptor-to-svelte';
33
37
  import { createAttachmentsSync } from '../../runes/attachments';
@@ -67,7 +71,10 @@
67
71
  // accept/reject control via the snap-back effect below — see TextInput's identical
68
72
  // mechanism (text-input/index.svelte) for why echoing unconditionally would break that.
69
73
  if (onValueChange === undefined) value = next;
70
- switchState = switchReducer(switchState, { type: 'native-reported', value: next });
74
+ switchState = switchReducer(switchState, {
75
+ type: 'native-reported',
76
+ value: next,
77
+ });
71
78
  }
72
79
 
73
80
  // Snap-back: when native reported a value the parent rejected (the value prop did not
@@ -102,7 +109,11 @@
102
109
  thumbColor,
103
110
  ios_backgroundColor,
104
111
  style,
105
- passthrough: { ...passthrough, class: className, onChange: handleChange },
112
+ passthrough: {
113
+ ...passthrough,
114
+ class: className,
115
+ onChange: handleChange,
116
+ },
106
117
  },
107
118
  PLATFORM,
108
119
  ),
@@ -120,4 +131,4 @@
120
131
  });
121
132
  </script>
122
133
 
123
- <symbiote-switch p={descriptor.props} bind:this={hostShim} />
134
+ <symbiote-switch-managed p={descriptor.props} bind:this={hostShim} />
@@ -48,6 +48,10 @@
48
48
  setInputFocused,
49
49
  setInputBlurred,
50
50
  type ISymbioteEvent,
51
+ type ISymbioteNode,
52
+ type IMeasureOnSuccess,
53
+ type IMeasureInWindowOnSuccess,
54
+ type IMeasureLayoutOnSuccess,
51
55
  } from '@symbiote-native/engine';
52
56
  import { createDescriptorChildrenSync } from '../../descriptor-to-svelte';
53
57
  import { createAttachmentsSync } from '../../runes/attachments';
@@ -89,7 +93,9 @@
89
93
  // plain, direct property read on the raw `$props()` proxy. `source` is a closed interface (no
90
94
  // index signature), so `Object.keys` + bracket-indexing would need an `as` cast; `Object.entries`
91
95
  // falls onto TS's `entries(o: {}): [string, any][]` overload instead, which needs no cast.
92
- function forwardProps(source: Omit<ITextInputProps, 'value'>): Record<string, unknown> {
96
+ function forwardProps(
97
+ source: Omit<ITextInputProps, 'value'>,
98
+ ): Record<string, unknown> {
93
99
  const resolved = resolveAccessibilityProps(source);
94
100
  const result: Record<string, unknown> = {};
95
101
  for (const [key, propValue] of Object.entries(resolved)) {
@@ -205,9 +211,17 @@
205
211
  if (engineNode === undefined) return;
206
212
  if (!shouldCommandText(lastNativeText, currentValue)) return;
207
213
  const selStart = rest.selection?.start ?? SELECTION_NONE;
208
- const selEnd = rest.selection?.end ?? rest.selection?.start ?? SELECTION_NONE;
209
- dlog(`TextInput setTextAndSelection count=${count} text=${JSON.stringify(currentValue)}`);
210
- dispatchViewCommand(engineNode, 'setTextAndSelection', [count, currentValue, selStart, selEnd]);
214
+ const selEnd =
215
+ rest.selection?.end ?? rest.selection?.start ?? SELECTION_NONE;
216
+ dlog(
217
+ `TextInput setTextAndSelection count=${count} text=${JSON.stringify(currentValue)}`,
218
+ );
219
+ dispatchViewCommand(engineNode, 'setTextAndSelection', [
220
+ count,
221
+ currentValue,
222
+ selStart,
223
+ selEnd,
224
+ ]);
211
225
  lastNativeText = currentValue;
212
226
  });
213
227
 
@@ -217,7 +231,8 @@
217
231
  // controlled-write effect above relies on.
218
232
  $effect(() => {
219
233
  const engineNode = hostShim?.engineNode;
220
- if (autoFocused || engineNode === undefined || rest.autoFocus !== true) return;
234
+ if (autoFocused || engineNode === undefined || rest.autoFocus !== true)
235
+ return;
221
236
  autoFocused = true;
222
237
  dlog('TextInput autoFocus -> focus command');
223
238
  dispatchViewCommand(engineNode, 'focus', []);
@@ -241,7 +256,12 @@
241
256
  export function clear(): void {
242
257
  const engineNode = hostShim?.engineNode;
243
258
  if (engineNode === undefined) return;
244
- dispatchViewCommand(engineNode, 'setTextAndSelection', [mostRecentEventCount, '', 0, 0]);
259
+ dispatchViewCommand(engineNode, 'setTextAndSelection', [
260
+ mostRecentEventCount,
261
+ '',
262
+ 0,
263
+ 0,
264
+ ]);
245
265
  lastNativeText = '';
246
266
  }
247
267
 
@@ -261,6 +281,43 @@
261
281
  ]);
262
282
  }
263
283
 
284
+ // The four host-ref methods, FORWARDED to the engine node rather than reimplemented.
285
+ //
286
+ // Without them a TextInput ref was poorer than any other host ref in the adapter: every other
287
+ // primitive hands back a node carrying measure/measureInWindow/measureLayout/setNativeProps, and
288
+ // this wrapper closed the node behind five methods of its own. The lowered path lost the opposite
289
+ // four, so the two paths were not "one narrowing the other" but two different surfaces.
290
+ // `ITextInputHandle` is the union of both halves, and `text-input.smoke.test.ts` derives what this
291
+ // component must expose from `buildTextInputHandle` — so a method added to core fails here rather
292
+ // than going missing on whichever path an app happened to take.
293
+ //
294
+ // Not typed as `satisfies ITextInputHandle`: these are instance-script exports, so the component's
295
+ // imperative surface is structurally invisible to the compiler — which is exactly why the test is
296
+ // the guard here and the type is the guard on React, Angular and Solid.
297
+ export function measure(callback: IMeasureOnSuccess): void {
298
+ hostShim?.engineNode?.measure(callback);
299
+ }
300
+
301
+ export function measureInWindow(callback: IMeasureInWindowOnSuccess): void {
302
+ hostShim?.engineNode?.measureInWindow(callback);
303
+ }
304
+
305
+ export function measureLayout(
306
+ relativeToNativeNode: ISymbioteNode | number,
307
+ onSuccess: IMeasureLayoutOnSuccess,
308
+ onFail?: () => void,
309
+ ): void {
310
+ hostShim?.engineNode?.measureLayout(
311
+ relativeToNativeNode,
312
+ onSuccess,
313
+ onFail,
314
+ );
315
+ }
316
+
317
+ export function setNativeProps(nativeProps: Record<string, unknown>): void {
318
+ hostShim?.engineNode?.setNativeProps(nativeProps);
319
+ }
320
+
264
321
  const descriptor = $derived(
265
322
  renderTextInput({
266
323
  multiline: isMultiline,
@@ -293,8 +350,23 @@
293
350
  });
294
351
  </script>
295
352
 
353
+ <!--
354
+ The `-managed` tags, matching `renderTextInput`'s intrinsics. They resolve to the SAME native view
355
+ as the plain ones; the separate spelling is what keeps the host behavior off this node. The machine
356
+ is registered against `symbiote-text-input` / `…-multiline`, which only the LOWERING transform
357
+ emits, and this wrapper runs the same lifecycle itself (focus/blur mirror, event counter,
358
+ controlled write, autoFocus) — so sharing a tag would give one node two owners.
359
+
360
+ Written literally rather than as `<svelte:element this={descriptor.type}>` for the reason the
361
+ header records, which means this pair does NOT follow `render-text-input.ts` on its own: if those
362
+ intrinsics are renamed again, this file has to move with them. `text-input-tag.test.ts` is what
363
+ makes that a failing test instead of a silent double-owner.
364
+ -->
296
365
  {#if isMultiline}
297
- <symbiote-text-input-multiline p={descriptor.props} bind:this={hostShim} />
366
+ <symbiote-text-input-multiline-managed
367
+ p={descriptor.props}
368
+ bind:this={hostShim}
369
+ />
298
370
  {:else}
299
- <symbiote-text-input p={descriptor.props} bind:this={hostShim} />
371
+ <symbiote-text-input-managed p={descriptor.props} bind:this={hostShim} />
300
372
  {/if}
@@ -1,12 +1,17 @@
1
1
  import type { ITextInputProps } from './text-input-props';
2
2
  export type { ITextInputProps };
3
3
  export type { ITextInputHandle } from '@symbiote-native/components';
4
+ import { type ISymbioteNode, type IMeasureOnSuccess, type IMeasureInWindowOnSuccess, type IMeasureLayoutOnSuccess } from '@symbiote-native/engine';
4
5
  declare const Index: import("svelte").Component<ITextInputProps, {
5
6
  focus: () => void;
6
7
  blur: () => void;
7
8
  clear: () => void;
8
9
  isFocused: () => boolean;
9
10
  setSelection: (start: number, end: number) => void;
11
+ measure: (callback: IMeasureOnSuccess) => void;
12
+ measureInWindow: (callback: IMeasureInWindowOnSuccess) => void;
13
+ measureLayout: (relativeToNativeNode: ISymbioteNode | number, onSuccess: IMeasureLayoutOnSuccess, onFail?: () => void) => void;
14
+ setNativeProps: (nativeProps: Record<string, unknown>) => void;
10
15
  }, "value">;
11
16
  type Index = ReturnType<typeof Index>;
12
17
  export default Index;