@symbiote-native/svelte 2.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/build/components/flat-list/index.svelte +5 -1
- package/build/components/keyboard-avoiding-view/index.svelte +10 -1
- package/build/components/pressable/pressable-props.d.ts +2 -0
- package/build/components/touchable-highlight/touchable-highlight-props.d.ts +2 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback-props.d.ts +1 -0
- package/build/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +3 -1
- package/build/components/virtualized-list/index.svelte +8 -7
- package/build/components/virtualized-list/virtualized-list-props.d.ts +2 -2
- package/build/components/virtualized-list/virtualized-list-props.js +24 -4
- package/build/create-portal/index.js +2 -2
- package/build/dom-shim/canonical-prop-names.js +3 -0
- package/build/dom-shim/element.d.ts +1 -0
- package/build/dom-shim/element.js +49 -13
- package/build/dom-shim/patch-globals.js +2 -2
- package/build/dom-shim/shim-node.d.ts +6 -0
- package/build/dom-shim/shim-node.js +48 -1
- package/build/dom-shim/text.js +3 -2
- package/build/host-instance.js +2 -2
- package/build/modules/animated/index.d.ts +23 -19
- package/package.json +7 -7
- package/build/dom-shim/fold-host-bag.d.ts +0 -1
- package/build/dom-shim/fold-host-bag.js +0 -7
package/README.md
CHANGED
|
@@ -29,9 +29,9 @@ npm install @symbiote-native/svelte react-native svelte
|
|
|
29
29
|
|
|
30
30
|
`react-native` and `svelte` stay your app's own top-level dependencies — this package only
|
|
31
31
|
replaces the JS renderer that drives them. Every component is a `.svelte` SFC — there's no TSX
|
|
32
|
-
alternative the way Vue offers — so the Metro wiring below isn't optional.
|
|
33
|
-
|
|
34
|
-
rather than a generator:
|
|
32
|
+
alternative the way Vue offers — so the Metro wiring below isn't optional. `npx @symbiote-native/cli new`
|
|
33
|
+
sets this up for a new app; wiring it into an existing one still follows
|
|
34
|
+
[`examples/svelte`](../../examples/svelte) rather than a generator:
|
|
35
35
|
|
|
36
36
|
- `metro.config.js` — point `babelTransformerPath` at
|
|
37
37
|
`@symbiote-native/svelte/metro-svelte-transformer` (compiles `.svelte` on the way into the
|
|
@@ -90,6 +90,10 @@
|
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
const rowKeyForRow = $derived(
|
|
94
|
+
(row: IRow<ItemT>): string => rowKeyExtractor(row, props.keyExtractor),
|
|
95
|
+
);
|
|
96
|
+
|
|
93
97
|
const rowOnViewableItemsChanged = $derived.by(() => {
|
|
94
98
|
const onChanged = props.onViewableItemsChanged;
|
|
95
99
|
if (onChanged === undefined) return undefined;
|
|
@@ -251,7 +255,7 @@
|
|
|
251
255
|
header={props.header}
|
|
252
256
|
footer={props.footer}
|
|
253
257
|
empty={props.empty}
|
|
254
|
-
keyExtractor={
|
|
258
|
+
keyExtractor={rowKeyForRow}
|
|
255
259
|
getItemLayout={props.getItemLayout}
|
|
256
260
|
horizontal={props.horizontal}
|
|
257
261
|
inverted={props.inverted}
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
import type { ShimElement } from '../../dom-shim';
|
|
31
31
|
import {
|
|
32
32
|
computeInset,
|
|
33
|
+
configureKeyboardAvoidingAnimation,
|
|
33
34
|
keyboardAvoidingEventNamesFor,
|
|
35
|
+
readKeyboardAnimationTiming,
|
|
34
36
|
readPrefersCrossFadeTransitions,
|
|
35
37
|
readKeyboardFrame,
|
|
36
38
|
readLayoutFrame,
|
|
@@ -68,11 +70,18 @@
|
|
|
68
70
|
// was built: Svelte compiles a destructured prop into a live getter, so a `behavior` changed
|
|
69
71
|
// after mount reaches the math on the next event, and 'height' mode feeds back the inset
|
|
70
72
|
// CURRENTLY applied (RN's this.state.bottom) to cancel the shrink its own wrapper caused.
|
|
73
|
+
const previousInset = inset;
|
|
71
74
|
const next = computeInset(frame, keyboard, keyboardVerticalOffset, {
|
|
72
75
|
behavior,
|
|
73
|
-
previousInset
|
|
76
|
+
previousInset,
|
|
74
77
|
prefersCrossFadeTransitions,
|
|
75
78
|
});
|
|
79
|
+
// RN's `_updateBottomIfNecessary` skips the animation when the inset did not change.
|
|
80
|
+
if (next !== previousInset)
|
|
81
|
+
configureKeyboardAvoidingAnimation(
|
|
82
|
+
readKeyboardAnimationTiming(payload),
|
|
83
|
+
enabled,
|
|
84
|
+
);
|
|
76
85
|
dlog(`KeyboardAvoidingView show -> inset ${next}`);
|
|
77
86
|
inset = next;
|
|
78
87
|
}
|
|
@@ -13,7 +13,9 @@ export interface IPressableProps extends IAccessibilityProps, IAriaProps {
|
|
|
13
13
|
disabled?: boolean;
|
|
14
14
|
focusable?: boolean;
|
|
15
15
|
cancelable?: boolean;
|
|
16
|
+
blockNativeResponder?: boolean;
|
|
16
17
|
hitSlop?: IRectOffset;
|
|
18
|
+
testOnly_pressed?: boolean;
|
|
17
19
|
pressRetentionOffset?: IRectOffset;
|
|
18
20
|
unstable_pressDelay?: number;
|
|
19
21
|
android_ripple?: IPressableAndroidRippleConfig;
|
|
@@ -2,6 +2,8 @@ import type { ITouchableBaseProps } from '../touchable-base-props';
|
|
|
2
2
|
export interface ITouchableHighlightProps extends ITouchableBaseProps {
|
|
3
3
|
activeOpacity?: number;
|
|
4
4
|
underlayColor?: string;
|
|
5
|
+
touchSoundDisabled?: boolean;
|
|
6
|
+
testOnly_pressed?: boolean;
|
|
5
7
|
onShowUnderlay?: () => void;
|
|
6
8
|
onHideUnderlay?: () => void;
|
|
7
9
|
}
|
|
@@ -40,10 +40,8 @@
|
|
|
40
40
|
|
|
41
41
|
<script lang="ts" generics="ItemT">
|
|
42
42
|
import {
|
|
43
|
-
DEFAULT_END_REACHED_THRESHOLD,
|
|
44
43
|
DEFAULT_INITIAL_NUM_TO_RENDER,
|
|
45
44
|
DEFAULT_MAX_TO_RENDER_PER_BATCH,
|
|
46
|
-
DEFAULT_START_REACHED_THRESHOLD,
|
|
47
45
|
DEFAULT_UPDATE_CELLS_BATCHING_PERIOD,
|
|
48
46
|
DEFAULT_WINDOW_SIZE,
|
|
49
47
|
EMPTY_OFFSET,
|
|
@@ -128,11 +126,9 @@
|
|
|
128
126
|
horizontal: props.horizontal === true,
|
|
129
127
|
inverted: props.inverted === true,
|
|
130
128
|
onEndReached: props.onEndReached,
|
|
131
|
-
onEndReachedThreshold:
|
|
132
|
-
props.onEndReachedThreshold ?? DEFAULT_END_REACHED_THRESHOLD,
|
|
129
|
+
onEndReachedThreshold: props.onEndReachedThreshold,
|
|
133
130
|
onStartReached: props.onStartReached,
|
|
134
|
-
onStartReachedThreshold:
|
|
135
|
-
props.onStartReachedThreshold ?? DEFAULT_START_REACHED_THRESHOLD,
|
|
131
|
+
onStartReachedThreshold: props.onStartReachedThreshold,
|
|
136
132
|
onRefresh: props.onRefresh,
|
|
137
133
|
refreshing: props.refreshing,
|
|
138
134
|
progressViewOffset: props.progressViewOffset,
|
|
@@ -252,7 +248,12 @@
|
|
|
252
248
|
const info = effect.info;
|
|
253
249
|
const map = effect.map;
|
|
254
250
|
const fire = (): void => {
|
|
255
|
-
for (const pair of pairs)
|
|
251
|
+
for (const pair of pairs) {
|
|
252
|
+
pair.onViewableItemsChanged({
|
|
253
|
+
...info,
|
|
254
|
+
viewabilityConfig: pair.viewabilityConfig,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
256
257
|
dispatch({ kind: 'viewable-fired', map });
|
|
257
258
|
};
|
|
258
259
|
if (viewableTimer !== null) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import type
|
|
2
|
+
import { type IStyleProp, type ISymbioteEvent, type ISymbioteNode, type IViewStyle } from '@symbiote-native/engine';
|
|
3
3
|
import { type IAccessibilityProps, type IAriaProps, type ISeparatorProps, type ISeparators, type IViewabilityConfig, type IViewabilityConfigCallbackPair, type IViewableItemsChangedInfo, type IVirtualizedListHandle } from '@symbiote-native/components';
|
|
4
4
|
import type { ISvelteClassValue } from '../../class-value';
|
|
5
5
|
export type { IVirtualizedListHandle };
|
|
@@ -67,4 +67,4 @@ export interface IVirtualizedListProps<ItemT> extends IAccessibilityProps, IAria
|
|
|
67
67
|
class?: ISvelteClassValue;
|
|
68
68
|
}
|
|
69
69
|
export type { ISeparators, ISeparatorProps, ISymbioteEvent, ISymbioteNode };
|
|
70
|
-
export declare function pickAccessibilityProps<T extends IAccessibilityProps & IAriaProps>(props: T): IAccessibilityProps;
|
|
70
|
+
export declare function pickAccessibilityProps<T extends IAccessibilityProps & IAriaProps>(props: T): IAccessibilityProps & IAriaProps;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ARIA_ALIAS_KEYS, } from '@symbiote-native/engine';
|
|
2
|
+
import {} from '@symbiote-native/components';
|
|
2
3
|
// Every IAccessibilityProps field, named explicitly (the object-bag convention this adapter's
|
|
3
4
|
// custom-element host tags require — svelte-adapter-dom-shim skill §3g(c) — forbids a raw
|
|
4
5
|
// `...rest` spread landing on a symbiote-* tag, so this mirrors React's `...accessibilityRest`
|
|
5
6
|
// field-by-field instead of reusing its spread). Shared by every list component (VirtualizedList's
|
|
6
7
|
// own host-bag construction AND FlatList's/VirtualizedSectionList's/SectionList's component-to-
|
|
7
8
|
// component forwarding down to VirtualizedList) so the field list lives in exactly one place.
|
|
8
|
-
// resolveAccessibilityProps
|
|
9
|
-
//
|
|
9
|
+
// It used to call `resolveAccessibilityProps` first, folding `aria-*`/`role` into their
|
|
10
|
+
// `accessibility*` twins so there were canonical names to pick BY NAME. That fold is the device's
|
|
11
|
+
// rule now, so the aria keys are picked and forwarded RAW instead (the loop at the end) and the
|
|
12
|
+
// engine folds once, at the leaf. Picking twice across a forwarding hop stays a no-op for the same
|
|
13
|
+
// reason folding twice was: a pick is idempotent.
|
|
10
14
|
export function pickAccessibilityProps(props) {
|
|
11
|
-
const resolved =
|
|
15
|
+
const resolved = props;
|
|
12
16
|
const picked = {};
|
|
13
17
|
if (resolved.testID !== undefined)
|
|
14
18
|
picked.testID = resolved.testID;
|
|
@@ -73,5 +77,21 @@ export function pickAccessibilityProps(props) {
|
|
|
73
77
|
picked.onMagicTap = resolved.onMagicTap;
|
|
74
78
|
if (resolved.onAccessibilityEscape !== undefined)
|
|
75
79
|
picked.onAccessibilityEscape = resolved.onAccessibilityEscape;
|
|
80
|
+
// THE ARIA HALF, forwarded RAW and DERIVED from the engine's own list rather than named again
|
|
81
|
+
// here. `ARIA_ALIAS_KEYS` is exported for exactly this — its comment says a second hand-written
|
|
82
|
+
// copy is what `adapter-parity-audit.md` records going stale one member at a time — so this loop
|
|
83
|
+
// gains a new alias the day the engine does.
|
|
84
|
+
//
|
|
85
|
+
// Raw, because the fold is the device's rule (`foldAriaProps`, `SymbioteFabricProps.cpp`) and it
|
|
86
|
+
// reads these names literally off the bag it commits. Folding them here would be a second
|
|
87
|
+
// implementation of it, and the one this file used to hold: `resolveAccessibilityProps`.
|
|
88
|
+
for (const key of ARIA_ALIAS_KEYS) {
|
|
89
|
+
const value = props[key];
|
|
90
|
+
// `Object.assign` with a computed key, not `picked[key] = value`: the key is a union and the
|
|
91
|
+
// value is its correlated member type, which TypeScript cannot check across a loop. This is the
|
|
92
|
+
// spelling that stays sound without an `as`.
|
|
93
|
+
if (value !== undefined)
|
|
94
|
+
Object.assign(picked, { [key]: value });
|
|
95
|
+
}
|
|
76
96
|
return picked;
|
|
77
97
|
}
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
// Node identity | nodes are MOVED (the old | content is re-created per TunnelOut
|
|
56
56
|
// | parent empties) |
|
|
57
57
|
import { pop, push, snippet, user_effect } from 'svelte/internal/client';
|
|
58
|
-
import { appendChild as engineAppendChild, removeChild as engineRemoveChild, dlog, isSymbioteNode, SymbioteSurface, } from '@symbiote-native/engine';
|
|
58
|
+
import { appendChild as engineAppendChild, removeChild as engineRemoveChild, componentOf, dlog, isSymbioteNode, SymbioteSurface, } from '@symbiote-native/engine';
|
|
59
59
|
import { ShimComment, ShimElement, ShimNode } from '../dom-shim/index.js';
|
|
60
60
|
// The Svelte flavour of React's `isSymbioteNode` guard, and it needs its own wording: React tells
|
|
61
61
|
// the caller to check for a forgotten `.current`, which does not exist here. A Svelte host ref is
|
|
@@ -75,7 +75,7 @@ function targetLabel(target) {
|
|
|
75
75
|
return `surface#${target.rootTag}`;
|
|
76
76
|
if (target instanceof ShimElement)
|
|
77
77
|
return target.tagName;
|
|
78
|
-
return target
|
|
78
|
+
return componentOf(target);
|
|
79
79
|
}
|
|
80
80
|
// Attach the fragment host under `target` and hand back the matching detach. Three branches
|
|
81
81
|
// because the three target kinds sit at different layers:
|
|
@@ -79,6 +79,7 @@ export const CANONICAL_PROP_NAMES = [
|
|
|
79
79
|
'backgroundColor',
|
|
80
80
|
'barStyle',
|
|
81
81
|
'behavior',
|
|
82
|
+
'blockNativeResponder',
|
|
82
83
|
'blurOnSubmit',
|
|
83
84
|
'blurRadius',
|
|
84
85
|
'bounces',
|
|
@@ -178,6 +179,7 @@ export const CANONICAL_PROP_NAMES = [
|
|
|
178
179
|
'onAccessibilityEscape',
|
|
179
180
|
'onAccessibilityTap',
|
|
180
181
|
'onBlur',
|
|
182
|
+
'onChangeText',
|
|
181
183
|
'onContentSizeChange',
|
|
182
184
|
'onDismiss',
|
|
183
185
|
'onEndEditing',
|
|
@@ -290,6 +292,7 @@ export const CANONICAL_PROP_NAMES = [
|
|
|
290
292
|
'submitBehavior',
|
|
291
293
|
'supportedOrientations',
|
|
292
294
|
'testID',
|
|
295
|
+
'testOnly_pressed',
|
|
293
296
|
'textAlign',
|
|
294
297
|
'textContentType',
|
|
295
298
|
'thumbColor',
|
|
@@ -26,7 +26,6 @@ import { createElement, dlog, routeProp, toPublicInstance, } from '@symbiote-nat
|
|
|
26
26
|
import { descriptorFor } from '@symbiote-native/components';
|
|
27
27
|
import { normalizeSvelteClass } from '../class-value.js';
|
|
28
28
|
import { CANONICAL_BY_LOWER, CANONICAL_PROP_NAMES, } from './canonical-prop-names.js';
|
|
29
|
-
import { foldHostBag } from './fold-host-bag.js';
|
|
30
29
|
import { ShimNode } from './shim-node.js';
|
|
31
30
|
import { discoverStyleCacheKey } from './style-cache.js';
|
|
32
31
|
// The bag a not-yet-live element is diffed against in onMadeLive. A module constant rather than a
|
|
@@ -61,11 +60,19 @@ export class ShimElement extends ShimElementBase {
|
|
|
61
60
|
// The four attribute doors write `doorBag`, `set p` writes `pBag`, `lastBag` is the folded merge.
|
|
62
61
|
// Held in ONE object, `set p` deleted the `class` `from_tree` had written at clone time, so
|
|
63
62
|
// `<view p={handlers} class="x">` committed with no style at all and nothing was red. Unreachable
|
|
64
|
-
// while an element used one door, which every
|
|
63
|
+
// while an element used one door, which nearly every element does.
|
|
65
64
|
// Both LAZY: an element touching one door still allocates one object.
|
|
66
65
|
doorBag = undefined;
|
|
67
66
|
pBag = undefined;
|
|
68
67
|
lastBag = EMPTY_BAG;
|
|
68
|
+
// COPY-ON-WRITE ownership of `doorBag`. `true` only while THIS element is the sole holder of the
|
|
69
|
+
// object `doorBag` points at, which lets `writeBagKey` mutate it in place instead of re-copying it
|
|
70
|
+
// on every one of an element's own sequential door writes. `cloneNode` hands the SAME object to a
|
|
71
|
+
// second element (§ below) and must clear this on both sides the moment it does, or a write on
|
|
72
|
+
// one clone would mutate the bag a sibling clone still reads — see
|
|
73
|
+
// `prop-bag-diff.test.ts`'s "a write to one clone does not leak into a sibling clone" for the
|
|
74
|
+
// isolation this exists to preserve.
|
|
75
|
+
ownsDoorBag = false;
|
|
69
76
|
constructor(tagName, namespaceURI) {
|
|
70
77
|
super();
|
|
71
78
|
this.tagName = tagName;
|
|
@@ -78,7 +85,7 @@ export class ShimElement extends ShimElementBase {
|
|
|
78
85
|
// no-opping — a `style` attribute on a bare tag crashed the mount. LAZY, for the reason
|
|
79
86
|
// `.claude/rules/svelte-shim-is-the-per-node-create-path.md` records about the two Maps below it:
|
|
80
87
|
// an eager field is one object per element, ~9 000 per create, in the window where GC is the
|
|
81
|
-
// largest bucket. Only `set_style` touches this, and
|
|
88
|
+
// largest bucket. Only `set_style` touches this, and a bag-carrying element never takes it.
|
|
82
89
|
styleSlot = undefined;
|
|
83
90
|
get style() {
|
|
84
91
|
return (this.styleSlot ??= { cssText: '' });
|
|
@@ -120,12 +127,10 @@ export class ShimElement extends ShimElementBase {
|
|
|
120
127
|
applyBagDiff(this.engineNode, prev, next);
|
|
121
128
|
this.surface?.requestCommit();
|
|
122
129
|
}
|
|
123
|
-
// A
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
// alone. Routing the key makes `<view testID="x">` and `<view p={{ testID: 'x' }}>` the same
|
|
128
|
-
// commit, so the transform goes back to being the optimisation it is everywhere else.
|
|
130
|
+
// A tag's props arrive here one key at a time, and they used to stop here: an inert Map, nothing
|
|
131
|
+
// routed, nothing committed, nothing red. Routing the key is what makes `<view testID="x">` and
|
|
132
|
+
// `<view p={{ testID: 'x' }}>` the same commit — the bag spelling is an optimisation, not the
|
|
133
|
+
// only way to reach the engine.
|
|
129
134
|
//
|
|
130
135
|
// `value` is `unknown`, not `string`: Svelte's `set_attribute` hands the raw value straight
|
|
131
136
|
// through for a name with no prototype setter, so an object `style` or a number arrives
|
|
@@ -156,7 +161,17 @@ export class ShimElement extends ShimElementBase {
|
|
|
156
161
|
// Not `private`: the prototype accessors installed at the bottom of this file are the third
|
|
157
162
|
// writer, and they are defined from module scope because there are ~290 of them.
|
|
158
163
|
writeBagKey(name, value) {
|
|
159
|
-
|
|
164
|
+
// COW: reuse `doorBag` in place only while this element is its SOLE owner (never cloned since
|
|
165
|
+
// its last write) — `cloneNode` clears the flag on both sides the instant a bag becomes shared
|
|
166
|
+
// (§ above). A fresh `{}` is `own`ed immediately: nothing else can reach it yet.
|
|
167
|
+
// Narrowed through a LOCAL, not through `owned`: TypeScript discards an aliased condition
|
|
168
|
+
// whose operands include a mutable property (`this.ownsDoorBag`), so the `!== undefined` leg
|
|
169
|
+
// never reached `this.doorBag` at the use site and this line did not compile.
|
|
170
|
+
const reusable = this.doorBag !== undefined && this.ownsDoorBag ? this.doorBag : undefined;
|
|
171
|
+
const owned = reusable !== undefined;
|
|
172
|
+
const door = reusable ?? { ...this.doorBag };
|
|
173
|
+
this.ownsDoorBag = true;
|
|
174
|
+
const prevValue = door[name];
|
|
160
175
|
if (value === undefined)
|
|
161
176
|
delete door[name];
|
|
162
177
|
else
|
|
@@ -167,6 +182,21 @@ export class ShimElement extends ShimElementBase {
|
|
|
167
182
|
this.lastBag = next;
|
|
168
183
|
if (this.engineNode === undefined)
|
|
169
184
|
return; // not live yet — onMadeLive() replays in full
|
|
185
|
+
// `foldedBag()` hands `door` straight back, BY REFERENCE, whenever there is no `p` bag to merge
|
|
186
|
+
// it with (its own "no `pBag`" branch) — so when `door` was mutated IN PLACE above (`owned`),
|
|
187
|
+
// `next` and `door` are the same object post-mutation, and a full `prev`-vs-`next` diff would
|
|
188
|
+
// be comparing that object against itself and finding nothing changed. The one key just written
|
|
189
|
+
// is the whole diff in that shape, by construction, so route it directly and skip the diff loop
|
|
190
|
+
// — this is what makes the COW above safe rather than merely silent (see
|
|
191
|
+
// `prop-bag-diff.test.ts`'s "commits an attribute written after it is live" style cases, which
|
|
192
|
+
// caught the alternative — a no-op write — the first time this was tried without this branch).
|
|
193
|
+
if (owned && this.pBag === undefined) {
|
|
194
|
+
const nextValue = next[name];
|
|
195
|
+
if (prevValue !== nextValue)
|
|
196
|
+
routeProp(this.engineNode, name, nextValue);
|
|
197
|
+
this.surface?.requestCommit();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
170
200
|
applyBagDiff(this.engineNode, prev, next);
|
|
171
201
|
this.surface?.requestCommit();
|
|
172
202
|
}
|
|
@@ -176,10 +206,10 @@ export class ShimElement extends ShimElementBase {
|
|
|
176
206
|
const door = this.doorBag;
|
|
177
207
|
const bag = this.pBag;
|
|
178
208
|
if (door === undefined)
|
|
179
|
-
return
|
|
209
|
+
return normalizeBagClasses(bag ?? {});
|
|
180
210
|
if (bag === undefined)
|
|
181
|
-
return
|
|
182
|
-
return
|
|
211
|
+
return normalizeBagClasses(door);
|
|
212
|
+
return normalizeBagClasses({ ...bag, ...door });
|
|
183
213
|
}
|
|
184
214
|
// THE FIFTH DOOR, and it converges on the same bag as the other four. Svelte turns EVERY
|
|
185
215
|
// `on<Name>` attribute into `$.event()`, whatever the name means to us — so this receives both
|
|
@@ -207,6 +237,12 @@ export class ShimElement extends ShimElementBase {
|
|
|
207
237
|
clone.doorBag = this.doorBag;
|
|
208
238
|
clone.pBag = this.pBag;
|
|
209
239
|
clone.lastBag = this.lastBag;
|
|
240
|
+
// `doorBag` (if any) now has TWO owners. Neither may mutate it in place from here — the next
|
|
241
|
+
// write on either side must copy first and only then reclaims exclusive ownership of ITS OWN
|
|
242
|
+
// copy. Without this a clone's first `setAttribute` would mutate the master's (and every other
|
|
243
|
+
// sibling clone's) committed props in place.
|
|
244
|
+
this.ownsDoorBag = false;
|
|
245
|
+
clone.ownsDoorBag = false;
|
|
210
246
|
if (deep === true) {
|
|
211
247
|
for (const child of this.children)
|
|
212
248
|
clone.appendChild(child.cloneNode(true));
|
|
@@ -72,8 +72,8 @@ const FAKE_CUSTOM_ELEMENT_REGISTRY = {
|
|
|
72
72
|
// rather than by reading. Svelte's `event()` (dom/elements/events.js) ends its teardown check with
|
|
73
73
|
// a BARE `dom instanceof HTMLMediaElement`, no `typeof` guard, so an undeclared global throws
|
|
74
74
|
// ReferenceError instead of yielding `undefined`. It is reached by every `on<Name>={fn}` an app
|
|
75
|
-
// writes on a bare tag, which is why nothing hit it while
|
|
76
|
-
//
|
|
75
|
+
// writes on a bare tag, which is why nothing hit it while every handler went through the `p` bag.
|
|
76
|
+
// Nothing of ours ever derives from it, so an empty class is enough:
|
|
77
77
|
// `instanceof` only has to answer false.
|
|
78
78
|
class FakeHTMLMediaElement {
|
|
79
79
|
}
|
|
@@ -3,6 +3,10 @@ type IDocumentLike = {
|
|
|
3
3
|
createTextNode(value: string): ShimNode;
|
|
4
4
|
};
|
|
5
5
|
export declare function registerShimDocumentFactory(factory: () => IDocumentLike): void;
|
|
6
|
+
export declare function takeShimScanStats(): {
|
|
7
|
+
calls: number;
|
|
8
|
+
scanned: number;
|
|
9
|
+
};
|
|
6
10
|
export declare abstract class ShimNode {
|
|
7
11
|
parent: ShimNode | null;
|
|
8
12
|
children: ShimNode[];
|
|
@@ -25,6 +29,8 @@ export declare abstract class ShimNode {
|
|
|
25
29
|
remove(): void;
|
|
26
30
|
removeChild<T extends ShimNode>(child: T): T;
|
|
27
31
|
set textContent(value: string);
|
|
32
|
+
private indexNearEnd;
|
|
33
|
+
private scannedIndexOf;
|
|
28
34
|
private insertOne;
|
|
29
35
|
}
|
|
30
36
|
export {};
|
|
@@ -25,6 +25,31 @@ function shimDocument() {
|
|
|
25
25
|
}
|
|
26
26
|
return documentFactory();
|
|
27
27
|
}
|
|
28
|
+
// How many insertBefore ref lookups fell through `indexNearEnd`'s bounded window into the full,
|
|
29
|
+
// unbounded `children.indexOf` scan, and how much of the array each one walked. Read-and-zeroed
|
|
30
|
+
// through `takeShimScanStats`, same shape as `core/engine/src/node.ts`'s `propStats`. This is the
|
|
31
|
+
// JS-side twin of a question F-80 already answered for the engine's own `OP_INSERT_BEFORE`
|
|
32
|
+
// (`std::find` over `siblings`, exonerated as too small): before `indexNearEnd` existed, Svelte's
|
|
33
|
+
// OWN reconciler paid the identical linear scan a second time, one layer up, on every keyed
|
|
34
|
+
// `{#each}` append — measured at 1.5M comparisons for a 1 000-row Append onto 1 000 standing
|
|
35
|
+
// (`each-append-scan-cost.probe.test.ts`). A zero reading here now means every ref this run
|
|
36
|
+
// needed was found in the tail window; a nonzero one means something inserts against a ref
|
|
37
|
+
// further from the end than `TAIL_SCAN_WINDOW` covers, which is real work this counter still
|
|
38
|
+
// prices honestly rather than hiding behind the fast path.
|
|
39
|
+
const shimScanStats = { calls: 0, scanned: 0 };
|
|
40
|
+
// Covers the measured case (a trailing block/component boundary anchor, one small constant hop
|
|
41
|
+
// from the true end) with room to spare, without making the fallback-miss case pay a meaningfully
|
|
42
|
+
// bigger constant before it gives up and scans properly.
|
|
43
|
+
const TAIL_SCAN_WINDOW = 8;
|
|
44
|
+
export function takeShimScanStats() {
|
|
45
|
+
const snapshot = {
|
|
46
|
+
calls: shimScanStats.calls,
|
|
47
|
+
scanned: shimScanStats.scanned,
|
|
48
|
+
};
|
|
49
|
+
shimScanStats.calls = 0;
|
|
50
|
+
shimScanStats.scanned = 0;
|
|
51
|
+
return snapshot;
|
|
52
|
+
}
|
|
28
53
|
export class ShimNode {
|
|
29
54
|
parent = null;
|
|
30
55
|
children = [];
|
|
@@ -131,6 +156,28 @@ export class ShimNode {
|
|
|
131
156
|
this.appendChild(shimDocument().createTextNode(value));
|
|
132
157
|
}
|
|
133
158
|
}
|
|
159
|
+
// Bounded backward scan, O(TAIL_SCAN_WINDOW) not O(children.length). Covers the case that
|
|
160
|
+
// dominates a real Append: a keyed `{#each}` growing at the tail inserts every new row via
|
|
161
|
+
// `newRow.before(ref)` where `ref` is the block's own closing boundary anchor — measured
|
|
162
|
+
// (`each-append-scan-cost.probe.test.ts`) sitting a SMALL, constant distance from the true end
|
|
163
|
+
// (one extra trailing anchor from the component's own root fragment in the measured case, not
|
|
164
|
+
// necessarily always exactly one — hence a window, not a hardcoded offset). Returns `undefined`
|
|
165
|
+
// on a miss so the caller falls back to the full, always-correct scan; this never changes the
|
|
166
|
+
// result, only how cheaply the common case finds it.
|
|
167
|
+
indexNearEnd(ref) {
|
|
168
|
+
const { children } = this;
|
|
169
|
+
const start = Math.max(0, children.length - TAIL_SCAN_WINDOW);
|
|
170
|
+
for (let i = children.length - 1; i >= start; i -= 1) {
|
|
171
|
+
if (children[i] === ref)
|
|
172
|
+
return i;
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
scannedIndexOf(ref) {
|
|
177
|
+
shimScanStats.calls += 1;
|
|
178
|
+
shimScanStats.scanned += this.children.length;
|
|
179
|
+
return this.children.indexOf(ref);
|
|
180
|
+
}
|
|
134
181
|
insertOne(node, ref) {
|
|
135
182
|
detachFromParent(node);
|
|
136
183
|
node.parent = this;
|
|
@@ -138,7 +185,7 @@ export class ShimNode {
|
|
|
138
185
|
this.children.push(node);
|
|
139
186
|
}
|
|
140
187
|
else {
|
|
141
|
-
const index = this.
|
|
188
|
+
const index = this.indexNearEnd(ref) ?? this.scannedIndexOf(ref);
|
|
142
189
|
this.children.splice(index < 0 ? this.children.length : index, 0, node);
|
|
143
190
|
}
|
|
144
191
|
if (this.engineNode !== undefined && this.surface !== undefined) {
|
package/build/dom-shim/text.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// Fabric (the reason Vue's renderer maps it to an anchor rather than an empty text); and a raw
|
|
17
17
|
// text child of a non-Text parent is the invalid "text outside <Text>" shape. Found by
|
|
18
18
|
// native-node-parity.test.ts, which diffs committed native trees against the Vue adapter.
|
|
19
|
-
import { appendChild as engineAppendChild, createAnchor, createRawText, insertBefore as engineInsertBefore, isAnchor, removeChild as engineRemoveChild, setText, } from '@symbiote-native/engine';
|
|
19
|
+
import { isTextContainer, appendChild as engineAppendChild, createAnchor, createRawText, insertBefore as engineInsertBefore, isAnchor, removeChild as engineRemoveChild, setText, } from '@symbiote-native/engine';
|
|
20
20
|
import { ShimNode } from './shim-node.js';
|
|
21
21
|
// Deliberately WIDER than Svelte's own whitespace class. `svelte/src/compiler/phases/patterns.js`
|
|
22
22
|
// uses /[^ \t\r\n]/ and says why: "Not \S because that also removes explicit whitespace defined
|
|
@@ -49,7 +49,8 @@ const WHITESPACE_ONLY = /^[\s\u200b-\u200d\ufeff]+$/;
|
|
|
49
49
|
function isFormattingWhitespace(value, parent) {
|
|
50
50
|
if (!WHITESPACE_ONLY.test(value))
|
|
51
51
|
return false;
|
|
52
|
-
|
|
52
|
+
const engineNode = parent?.engineNode;
|
|
53
|
+
return engineNode === undefined || !isTextContainer(engineNode);
|
|
53
54
|
}
|
|
54
55
|
export class ShimText extends ShimNode {
|
|
55
56
|
value;
|
package/build/host-instance.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// RN's own findNodeHandle has). The node -> tag resolution itself belongs to the engine
|
|
10
10
|
// (getNativeTag, keyed on the raw node in the commit mirror); this only adapts the
|
|
11
11
|
// Svelte-shaped input (a ShimElement) onto it.
|
|
12
|
-
import { getNativeTag, isSymbioteNode, toPublicInstance, dlog, } from '@symbiote-native/engine';
|
|
12
|
+
import { componentOf, getNativeTag, isSymbioteNode, toPublicInstance, dlog, } from '@symbiote-native/engine';
|
|
13
13
|
// `engineNode` alone does NOT identify a ShimElement: it is declared on the shared ShimNode base
|
|
14
14
|
// (dom-shim/shim-node.ts), so a ShimText / ShimComment / ShimDocumentFragment satisfies it too,
|
|
15
15
|
// and none of those is a host ref an interop library can hand back. `tagName` is ShimElement's
|
|
@@ -50,7 +50,7 @@ export function findNodeHandle(componentOrHandle) {
|
|
|
50
50
|
const node = componentOrHandle.engineNode;
|
|
51
51
|
if (node !== undefined && isSymbioteNode(node)) {
|
|
52
52
|
const tag = getNativeTag(node) ?? null;
|
|
53
|
-
dlog(`findNodeHandle: component=${node
|
|
53
|
+
dlog(`findNodeHandle: component=${componentOf(node)} tag=${tag}`);
|
|
54
54
|
return tag;
|
|
55
55
|
}
|
|
56
56
|
dlog('findNodeHandle: hostShim not live yet (engineNode undefined)');
|
|
@@ -3,25 +3,7 @@ declare const drivers: {
|
|
|
3
3
|
Value: typeof AnimatedValue;
|
|
4
4
|
ValueXY: typeof AnimatedValueXY;
|
|
5
5
|
Color: typeof AnimatedColor;
|
|
6
|
-
Easing:
|
|
7
|
-
step0(n: number): number;
|
|
8
|
-
step1(n: number): number;
|
|
9
|
-
linear(t: number): number;
|
|
10
|
-
ease(t: number): number;
|
|
11
|
-
quad(t: number): number;
|
|
12
|
-
cubic(t: number): number;
|
|
13
|
-
poly(n: number): import("@symbiote-native/engine").IEasingFunction;
|
|
14
|
-
sin(t: number): number;
|
|
15
|
-
circle(t: number): number;
|
|
16
|
-
exp(t: number): number;
|
|
17
|
-
elastic(bounciness?: number): import("@symbiote-native/engine").IEasingFunction;
|
|
18
|
-
back(s?: number): import("@symbiote-native/engine").IEasingFunction;
|
|
19
|
-
bounce(t: number): number;
|
|
20
|
-
bezier(x1: number, y1: number, x2: number, y2: number): import("@symbiote-native/engine").IEasingFunction;
|
|
21
|
-
in(easing: import("@symbiote-native/engine").IEasingFunction): import("@symbiote-native/engine").IEasingFunction;
|
|
22
|
-
out(easing: import("@symbiote-native/engine").IEasingFunction): import("@symbiote-native/engine").IEasingFunction;
|
|
23
|
-
inOut(easing: import("@symbiote-native/engine").IEasingFunction): import("@symbiote-native/engine").IEasingFunction;
|
|
24
|
-
};
|
|
6
|
+
Easing: import("@symbiote-native/engine").IEasing;
|
|
25
7
|
timing: typeof timing;
|
|
26
8
|
spring: typeof spring;
|
|
27
9
|
decay: typeof decay;
|
|
@@ -39,6 +21,28 @@ declare const drivers: {
|
|
|
39
21
|
event: typeof event;
|
|
40
22
|
forkEvent: typeof forkEvent;
|
|
41
23
|
unforkEvent: typeof unforkEvent;
|
|
24
|
+
} | {
|
|
25
|
+
Value: typeof AnimatedValue;
|
|
26
|
+
ValueXY: typeof AnimatedValueXY;
|
|
27
|
+
Color: typeof AnimatedColor;
|
|
28
|
+
Easing: import("@symbiote-native/engine").IEasing;
|
|
29
|
+
timing: (value: AnimatedValue, config: import("@symbiote-native/engine").ITimingConfig) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
30
|
+
spring: (value: AnimatedValue, config: import("@symbiote-native/engine").ISpringConfig) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
31
|
+
decay: (_value: AnimatedValue, _config: import("@symbiote-native/engine").IDecayConfig) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
32
|
+
parallel: (animations: import("@symbiote-native/engine").ICompositeAnimation[], _config?: import("@symbiote-native/engine").IParallelConfig) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
33
|
+
sequence: (animations: import("@symbiote-native/engine").ICompositeAnimation[]) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
34
|
+
stagger: (_time: number, animations: import("@symbiote-native/engine").ICompositeAnimation[]) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
35
|
+
loop: (_animation: import("@symbiote-native/engine").ICompositeAnimation) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
36
|
+
delay: (_time: number) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
37
|
+
add: typeof add;
|
|
38
|
+
subtract: typeof subtract;
|
|
39
|
+
multiply: typeof multiply;
|
|
40
|
+
divide: typeof divide;
|
|
41
|
+
modulo: typeof modulo;
|
|
42
|
+
diffClamp: typeof diffClamp;
|
|
43
|
+
event: typeof event;
|
|
44
|
+
forkEvent: typeof forkEvent;
|
|
45
|
+
unforkEvent: typeof unforkEvent;
|
|
42
46
|
};
|
|
43
47
|
interface IAnimatedNamespace {
|
|
44
48
|
FlatList: typeof import('../../components/flat-list/index.svelte').default;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/svelte",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "SymbioteNative's Svelte adapter — a DOM shim over stock compiled Svelte output driving real native iOS/Android views through the same engine as every other adapter.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -73,22 +73,22 @@
|
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
75
|
"typescript": "~6.0.0",
|
|
76
|
-
"@symbiote-native/components": "
|
|
76
|
+
"@symbiote-native/components": "3.0.1",
|
|
77
77
|
"@symbiote-native/css-parser": "0.5.0"
|
|
78
78
|
},
|
|
79
79
|
"peerDependencies": {
|
|
80
80
|
"svelte": ">=5.56.0",
|
|
81
81
|
"react-native": ">=0.86",
|
|
82
|
-
"@symbiote-native/engine": "^
|
|
82
|
+
"@symbiote-native/engine": "^1.2.0"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@vue/runtime-core": "^3.5.13",
|
|
86
86
|
"react": "19.2.3",
|
|
87
87
|
"svelte": "^5.56.0",
|
|
88
|
-
"@symbiote-native/engine": "
|
|
89
|
-
"@symbiote-native/react": "
|
|
90
|
-
"@symbiote-native/test-utils": "0.
|
|
91
|
-
"@symbiote-native/vue": "
|
|
88
|
+
"@symbiote-native/engine": "1.2.0",
|
|
89
|
+
"@symbiote-native/react": "3.0.1",
|
|
90
|
+
"@symbiote-native/test-utils": "0.4.1",
|
|
91
|
+
"@symbiote-native/vue": "3.0.1"
|
|
92
92
|
},
|
|
93
93
|
"scripts": {
|
|
94
94
|
"typecheck": "tsc --build",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { foldHostBag, FOLD_PLAN_BY_TAG as BY_TAG, type IHostBag, } from '@symbiote-native/components/fold-host-bag';
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
// Moved to `@symbiote-native/components/fold-host-bag` (2026-09-01) and kept here as a re-export.
|
|
2
|
-
//
|
|
3
|
-
// It left this adapter the moment a SECOND adapter needed it: React's wrappers are being replaced
|
|
4
|
-
// by bare intrinsic tags, and a bare tag has no wrapper to fold in — the same "third path" this
|
|
5
|
-
// file was written for, arriving in an adapter that has no lowering transform either. A per-adapter
|
|
6
|
-
// copy of a fold driven by a shared spec is the duplication `<adapters_stay_thin>` exists to stop.
|
|
7
|
-
export { foldHostBag, FOLD_PLAN_BY_TAG as BY_TAG, } from '@symbiote-native/components/fold-host-bag';
|