@symbiote-native/svelte 0.3.0 → 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.
- package/build/components/pressable/index.svelte +45 -27
- package/build/components/pressable/index.svelte.d.ts +5 -1
- package/build/components/safe-area-view-props.d.ts +1 -0
- package/build/components/switch/index.svelte +2 -2
- package/build/components/text-input/index.svelte +58 -2
- package/build/components/text-input/index.svelte.d.ts +5 -0
- package/build/components/touchable-highlight/index.svelte +1 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback.svelte +1 -1
- package/build/components/touchable-opacity/index.svelte +1 -0
- package/build/components/touchable-without-feedback/index.svelte +1 -0
- package/build/dom-shim/element.d.ts +9 -3
- package/build/dom-shim/element.js +77 -25
- package/build/dom-shim/fold-host-bag.d.ts +1 -0
- package/build/dom-shim/fold-host-bag.js +7 -0
- package/build/dom-shim/patch-globals.js +4 -2
- package/build/dom-shim/shim-node.js +11 -0
- package/build/host-instance.js +2 -3
- package/build/index.d.ts +1 -0
- package/build/index.js +4 -0
- package/build/preprocessor/lower-host-primitives.d.ts +10 -0
- package/build/preprocessor/lower-host-primitives.js +455 -0
- package/build/register.d.ts +1 -0
- package/build/register.js +33 -0
- package/build/state-style.d.ts +1 -0
- package/build/state-style.js +8 -0
- package/metro-svelte-transformer.cjs +47 -13
- package/package.json +28 -10
|
@@ -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
|
|
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,38 +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({
|
|
128
|
-
...rest,
|
|
129
|
-
accessibilityState: resolvedAccessibilityState,
|
|
130
|
-
}),
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
const resolvedStyle = $derived(
|
|
134
|
-
typeof style === 'function' ? style(state) : style,
|
|
135
|
-
);
|
|
136
|
-
|
|
137
131
|
// android_ripple rides a dedicated inner View; on iOS rippleProps() returns undefined, so the
|
|
138
132
|
// child renders unwrapped, no extra node — mirrors React's Pressable + touchable-native-feedback.
|
|
139
133
|
const ripple = $derived(
|
|
140
134
|
android_ripple !== undefined ? rippleProps(android_ripple) : undefined,
|
|
141
135
|
);
|
|
142
136
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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;
|
|
150
168
|
});
|
|
151
169
|
|
|
152
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
|
-
|
|
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;
|
|
@@ -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;
|
|
@@ -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
|
|
@@ -131,4 +131,4 @@
|
|
|
131
131
|
});
|
|
132
132
|
</script>
|
|
133
133
|
|
|
134
|
-
<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';
|
|
@@ -277,6 +281,43 @@
|
|
|
277
281
|
]);
|
|
278
282
|
}
|
|
279
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
|
+
|
|
280
321
|
const descriptor = $derived(
|
|
281
322
|
renderTextInput({
|
|
282
323
|
multiline: isMultiline,
|
|
@@ -309,8 +350,23 @@
|
|
|
309
350
|
});
|
|
310
351
|
</script>
|
|
311
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
|
+
-->
|
|
312
365
|
{#if isMultiline}
|
|
313
|
-
<symbiote-text-input-multiline
|
|
366
|
+
<symbiote-text-input-multiline-managed
|
|
367
|
+
p={descriptor.props}
|
|
368
|
+
bind:this={hostShim}
|
|
369
|
+
/>
|
|
314
370
|
{:else}
|
|
315
|
-
<symbiote-text-input p={descriptor.props} bind:this={hostShim} />
|
|
371
|
+
<symbiote-text-input-managed p={descriptor.props} bind:this={hostShim} />
|
|
316
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;
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { type ISymbioteNode } from '@symbiote-native/engine';
|
|
2
2
|
import { ShimNode } from './shim-node';
|
|
3
3
|
export type IShimPropBag = Record<string, unknown>;
|
|
4
|
-
export declare class
|
|
4
|
+
export declare abstract class ShimElementBase extends ShimNode {
|
|
5
|
+
}
|
|
6
|
+
export declare class ShimElement extends ShimElementBase {
|
|
5
7
|
readonly tagName: string;
|
|
6
8
|
readonly namespaceURI: string | undefined;
|
|
7
|
-
private
|
|
8
|
-
private
|
|
9
|
+
private attributes;
|
|
10
|
+
private domListeners;
|
|
9
11
|
private lastBag;
|
|
10
12
|
constructor(tagName: string, namespaceURI?: string);
|
|
11
13
|
get nodeName(): string;
|
|
14
|
+
private styleSlot;
|
|
15
|
+
get style(): {
|
|
16
|
+
cssText: string;
|
|
17
|
+
};
|
|
12
18
|
get p(): IShimPropBag;
|
|
13
19
|
set p(bag: IShimPropBag | undefined);
|
|
14
20
|
setAttribute(name: string, value: string): void;
|
|
@@ -10,10 +10,15 @@
|
|
|
10
10
|
// A per-key diff in the setter is MANDATORY (§3g(c)), not an optimization: Svelte's
|
|
11
11
|
// `set_custom_element_data` has no early-out guard and runs on every effect re-fire, so
|
|
12
12
|
// without the diff every prop gets rewritten whenever any one of them changes.
|
|
13
|
-
import { createElement, dlog,
|
|
13
|
+
import { createElement, dlog, routeProp, setEventListener, toPublicInstance, } from '@symbiote-native/engine';
|
|
14
14
|
import { descriptorFor } from '@symbiote-native/components';
|
|
15
15
|
import { normalizeSvelteClass } from '../class-value.js';
|
|
16
|
+
import { foldHostBag } from './fold-host-bag.js';
|
|
16
17
|
import { ShimNode } from './shim-node.js';
|
|
18
|
+
// The bag a not-yet-live element is diffed against in onMadeLive. A module constant rather than a
|
|
19
|
+
// fresh `{}` per node: it is only ever read, and the identity lets applyBagDiff skip its second
|
|
20
|
+
// pass outright on the create path.
|
|
21
|
+
const EMPTY_BAG = Object.freeze({});
|
|
17
22
|
// Diagnostic-only: a process-wide sequence so every `set p` call across every shim element in a
|
|
18
23
|
// log dump is individually orderable against AnimatedProps reconcile#N / AnimatedView reduced#N
|
|
19
24
|
// / hostShim identity-change#N, to see which one is driving which.
|
|
@@ -21,12 +26,26 @@ let globalSetPSeq = 0;
|
|
|
21
26
|
// `on<Name>` handlers ride inside the prop bag (idiomatic Svelte 5 callback props — see the
|
|
22
27
|
// skill's §3g(c) "most of §5 collapses" note) — they are not addEventListener-style DOM
|
|
23
28
|
// listeners, so they are diffed and routed exactly like any other bag key, through routeProp.
|
|
24
|
-
|
|
29
|
+
// An empty layer that exists to be what `globalThis.Element` points at, and it is load-bearing.
|
|
30
|
+
// Svelte's `get_setters` (internal/client/dom/elements/attributes.js) walks from the ELEMENT
|
|
31
|
+
// INSTANCE up and STOPS when it reaches `Element.prototype`. While `Element` was `ShimElement`
|
|
32
|
+
// itself, the first prototype step was already the stop, so the walk collected nothing and `p` —
|
|
33
|
+
// which lives on `ShimElement.prototype`, one step past it — was invisible. `set_attributes` then
|
|
34
|
+
// fell through to `setAttribute`, which writes an inert Map, and every prop vanished with nothing
|
|
35
|
+
// red. The rule is therefore that `Element` must be a proper ANCESTOR of the class owning the
|
|
36
|
+
// setters, never that class: `.claude/rules/svelte-shim-element-global-must-be-an-ancestor.md`.
|
|
37
|
+
export class ShimElementBase extends ShimNode {
|
|
38
|
+
}
|
|
39
|
+
export class ShimElement extends ShimElementBase {
|
|
25
40
|
tagName;
|
|
26
41
|
namespaceURI;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
// Both LAZY, and that is a measured decision, not a style one. A lowered primitive carries its
|
|
43
|
+
// whole prop surface in the `p` bag, so neither map is ever written on the create path — but an
|
|
44
|
+
// eager field allocated two Maps per element regardless, 18 004 of them on a 1 000-row create,
|
|
45
|
+
// in a window where GC is the largest single bucket (29%).
|
|
46
|
+
attributes;
|
|
47
|
+
domListeners;
|
|
48
|
+
lastBag = EMPTY_BAG;
|
|
30
49
|
constructor(tagName, namespaceURI) {
|
|
31
50
|
super();
|
|
32
51
|
this.tagName = tagName;
|
|
@@ -35,66 +54,85 @@ export class ShimElement extends ShimNode {
|
|
|
35
54
|
get nodeName() {
|
|
36
55
|
return this.tagName;
|
|
37
56
|
}
|
|
57
|
+
// `set_style` writes `dom.style.cssText`, so a shim with no `.style` THROWS rather than
|
|
58
|
+
// no-opping — a `style` attribute on a bare tag crashed the mount. LAZY, for the reason
|
|
59
|
+
// `.claude/rules/svelte-shim-is-the-per-node-create-path.md` records about the two Maps below it:
|
|
60
|
+
// an eager field is one object per element, ~9 000 per create, in the window where GC is the
|
|
61
|
+
// largest bucket. Only `set_style` touches this, and no lowered element takes that path.
|
|
62
|
+
styleSlot;
|
|
63
|
+
get style() {
|
|
64
|
+
return (this.styleSlot ??= { cssText: '' });
|
|
65
|
+
}
|
|
38
66
|
// The single object-bag prop. The literal name is ours to choose (§3g(c)) — the adapter's
|
|
39
67
|
// own View.svelte/Text.svelte/… emit `<symbiote-view p={bag}>`; app code never sees it.
|
|
40
68
|
get p() {
|
|
41
69
|
return this.lastBag;
|
|
42
70
|
}
|
|
43
71
|
set p(bag) {
|
|
44
|
-
|
|
72
|
+
// The fold runs HERE rather than at element creation because an alias can arrive on an update
|
|
73
|
+
// (`id` bound to a signal), and it runs before the diff so `lastBag` is always the folded shape
|
|
74
|
+
// — otherwise a seeded default would look like a change on every single set.
|
|
75
|
+
const next = foldHostBag(this.tagName, normalizeBagClasses(bag ?? {}));
|
|
45
76
|
const prev = this.lastBag;
|
|
46
77
|
this.lastBag = next;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
// THUNK, not a string (see debug.ts's header): this setter runs once per element per create
|
|
79
|
+
// and the changed-key list is diagnostic only. Built eagerly it cost a Set, two key arrays, a
|
|
80
|
+
// join and a template literal on every one of 9 002 elements, with logging off.
|
|
81
|
+
dlog(() => `ShimElement set-p#${++globalSetPSeq} tag=${this.tagName} live=${this.engineNode !== undefined} ` +
|
|
82
|
+
`changedKeys=${diffKeys(prev, next).join(',')}`);
|
|
50
83
|
if (this.engineNode === undefined)
|
|
51
84
|
return; // not live yet — onMadeLive() replays `next` in full
|
|
52
85
|
applyBagDiff(this.engineNode, prev, next);
|
|
53
86
|
this.surface?.requestCommit();
|
|
54
87
|
}
|
|
55
88
|
setAttribute(name, value) {
|
|
56
|
-
this.attributes.set(name, value);
|
|
89
|
+
(this.attributes ??= new Map()).set(name, value);
|
|
57
90
|
}
|
|
58
91
|
getAttribute(name) {
|
|
59
|
-
return this.attributes
|
|
92
|
+
return this.attributes?.get(name) ?? null;
|
|
60
93
|
}
|
|
61
94
|
removeAttribute(name) {
|
|
62
|
-
this.attributes
|
|
95
|
+
this.attributes?.delete(name);
|
|
63
96
|
}
|
|
64
97
|
addEventListener(name, handler) {
|
|
65
|
-
this.domListeners.set(name, handler);
|
|
98
|
+
(this.domListeners ??= new Map()).set(name, handler);
|
|
66
99
|
if (this.engineNode !== undefined)
|
|
67
100
|
setEventListener(this.engineNode, name, handler);
|
|
68
101
|
}
|
|
69
102
|
removeEventListener(name) {
|
|
70
|
-
this.domListeners
|
|
103
|
+
this.domListeners?.delete(name);
|
|
71
104
|
if (this.engineNode !== undefined)
|
|
72
105
|
setEventListener(this.engineNode, name, undefined);
|
|
73
106
|
}
|
|
74
107
|
cloneNode(deep) {
|
|
75
108
|
const clone = new ShimElement(this.tagName, this.namespaceURI);
|
|
76
|
-
|
|
77
|
-
clone.attributes.
|
|
109
|
+
if (this.attributes !== undefined)
|
|
110
|
+
clone.attributes = new Map(this.attributes);
|
|
78
111
|
if (deep === true) {
|
|
79
112
|
for (const child of this.children)
|
|
80
113
|
clone.appendChild(child.cloneNode(true));
|
|
81
114
|
}
|
|
82
115
|
return clone;
|
|
83
116
|
}
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// getPublicInstance.
|
|
88
|
-
// the previously-bare createElement() call.
|
|
117
|
+
// measure/measureInWindow/measureLayout/setNativeProps/focus/blur — the imperative API a
|
|
118
|
+
// `bind:this` host ref hands back — ride on the engine node's prototype, so toPublicInstance is
|
|
119
|
+
// the identity and this reads the same as Vue's renderer (renderer/index.ts) and React's
|
|
120
|
+
// getPublicInstance.
|
|
89
121
|
createEngineNode() {
|
|
90
122
|
const descriptor = descriptorFor(this.tagName);
|
|
91
|
-
|
|
123
|
+
// The INTRINSIC TAG as the third argument, and this is the only place the tag alphabet still
|
|
124
|
+
// exists: `descriptor.component` is the Fabric view name (`symbiote-view` -> `RCTView`), so a
|
|
125
|
+
// host-behavior registry keyed by tag can only be reached from here. The argument defaults to
|
|
126
|
+
// `component`, so passing it changes nothing until a behavior is registered.
|
|
127
|
+
return toPublicInstance(createElement(descriptor.component, descriptor.isText, this.tagName));
|
|
92
128
|
}
|
|
93
129
|
onMadeLive() {
|
|
94
130
|
const engineNode = this.engineNode;
|
|
95
131
|
if (engineNode === undefined)
|
|
96
132
|
return;
|
|
97
|
-
applyBagDiff(engineNode,
|
|
133
|
+
applyBagDiff(engineNode, EMPTY_BAG, this.lastBag);
|
|
134
|
+
if (this.domListeners === undefined)
|
|
135
|
+
return;
|
|
98
136
|
for (const [name, handler] of this.domListeners)
|
|
99
137
|
setEventListener(engineNode, name, handler);
|
|
100
138
|
}
|
|
@@ -120,11 +158,25 @@ function normalizeBagClasses(bag) {
|
|
|
120
158
|
}
|
|
121
159
|
return next;
|
|
122
160
|
}
|
|
161
|
+
// Diagnostic only — the one caller is the `dlog` thunk above. The allocation-free twin below is
|
|
162
|
+
// what the hot path uses; keep them in step.
|
|
123
163
|
function diffKeys(prev, next) {
|
|
124
164
|
const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
|
|
125
165
|
return [...keys].filter(key => prev[key] !== next[key]);
|
|
126
166
|
}
|
|
167
|
+
// Deliberately NOT `for (const key of diffKeys(...))`: that materialized a Set, two key arrays,
|
|
168
|
+
// a spread and a filtered array per element. Two direct passes route exactly the same keys —
|
|
169
|
+
// changed-or-added from `next`, then dropped keys from `prev` as `undefined`, which is what
|
|
170
|
+
// `next[key]` evaluated to in the old shape.
|
|
127
171
|
function applyBagDiff(engineNode, prev, next) {
|
|
128
|
-
for (const key of
|
|
129
|
-
|
|
172
|
+
for (const key of Object.keys(next)) {
|
|
173
|
+
if (prev[key] !== next[key])
|
|
174
|
+
routeProp(engineNode, key, next[key]);
|
|
175
|
+
}
|
|
176
|
+
if (prev === EMPTY_BAG)
|
|
177
|
+
return;
|
|
178
|
+
for (const key of Object.keys(prev)) {
|
|
179
|
+
if (!(key in next))
|
|
180
|
+
routeProp(engineNode, key, undefined);
|
|
181
|
+
}
|
|
130
182
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { foldHostBag, FOLD_PLAN_BY_TAG as BY_TAG, type IHostBag, } from '@symbiote-native/components/fold-host-bag';
|
|
@@ -0,0 +1,7 @@
|
|
|
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';
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
//
|
|
25
25
|
// `customElements` IS patched, unlike the three above — see its own comment below.
|
|
26
26
|
import { dlog } from '@symbiote-native/engine';
|
|
27
|
-
import { ShimElement } from './element.js';
|
|
27
|
+
import { ShimElement, ShimElementBase } from './element.js';
|
|
28
28
|
import { ShimText } from './text.js';
|
|
29
29
|
import { ShimComment } from './comment.js';
|
|
30
30
|
import { ShimDocumentFragment } from './document-fragment.js';
|
|
@@ -70,7 +70,9 @@ export function patchGlobals() {
|
|
|
70
70
|
customElements: g.customElements,
|
|
71
71
|
};
|
|
72
72
|
g.Node = ShimNode;
|
|
73
|
-
|
|
73
|
+
// ShimElementBase, NOT ShimElement: `get_setters` stops AT `Element.prototype`, so pointing this
|
|
74
|
+
// at the class that owns `p` hides `p` from every `set_attributes` caller. See element.ts.
|
|
75
|
+
g.Element = ShimElementBase;
|
|
74
76
|
// Svelte's mandatory paths never distinguish HTMLElement/SVGElement from Element (we have
|
|
75
77
|
// no `<svg>` primitives), so both alias the same class — real, extensible prototypes either
|
|
76
78
|
// way, which is all `init_operations()` requires (§3a).
|
|
@@ -69,12 +69,23 @@ export class ShimNode {
|
|
|
69
69
|
for (const node of nodes)
|
|
70
70
|
this.appendChild(node);
|
|
71
71
|
}
|
|
72
|
+
// The fragment branch is the rare one — every ordinary insert is a single node — but
|
|
73
|
+
// `normalizeInsertable` allocated a one-element array for it regardless, 23 006 of them on a
|
|
74
|
+
// 1 000-row create. Test the flag first and hand the node straight to insertOne.
|
|
72
75
|
appendChild(node) {
|
|
76
|
+
if (!node.isDocumentFragment) {
|
|
77
|
+
this.insertOne(node, null);
|
|
78
|
+
return node;
|
|
79
|
+
}
|
|
73
80
|
for (const single of normalizeInsertable(node))
|
|
74
81
|
this.insertOne(single, null);
|
|
75
82
|
return node;
|
|
76
83
|
}
|
|
77
84
|
insertBefore(node, ref) {
|
|
85
|
+
if (!node.isDocumentFragment) {
|
|
86
|
+
this.insertOne(node, ref);
|
|
87
|
+
return node;
|
|
88
|
+
}
|
|
78
89
|
for (const single of normalizeInsertable(node))
|
|
79
90
|
this.insertOne(single, ref);
|
|
80
91
|
return node;
|
package/build/host-instance.js
CHANGED
|
@@ -22,9 +22,8 @@ function isShimElement(value) {
|
|
|
22
22
|
}
|
|
23
23
|
// The typed imperative handle (measure/measureInWindow/measureLayout/setNativeProps/focus/blur)
|
|
24
24
|
// a `bind:this` host ref carries — the Svelte twin of a React `ref.current`/Vue template ref
|
|
25
|
-
// already being an `IHostInstance`.
|
|
26
|
-
// toPublicInstance
|
|
27
|
-
// isHostInstance guard short-circuits); this helper exists only to give app code a correctly
|
|
25
|
+
// already being an `IHostInstance`. Every engine node carries those methods on its prototype, so
|
|
26
|
+
// the toPublicInstance call below is the identity; this helper exists only to give app code a correctly
|
|
28
27
|
// TYPED accessor off the SHIM value (`ShimElement`) instead of the bare `.engineNode` field,
|
|
29
28
|
// with no `as` cast at the call site.
|
|
30
29
|
export function hostInstance(shim) {
|
package/build/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import './register';
|
|
1
2
|
export { mount, unmount } from './render';
|
|
2
3
|
export { AppRegistry, setHostRegistrar } from './modules/app-registry';
|
|
3
4
|
export type { IComponentProvider, IWrapperComponentProvider, IAppParameters, IRunnable, IHostRegistrar, IRegistry, IHeadlessTask, ITaskProvider, ITaskCanceller, ITaskCancelProvider, } from './modules/app-registry';
|
package/build/index.js
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
// `descriptorToSvelte` bridge was ever needed — each component below is hand-authored Svelte
|
|
10
10
|
// markup mirroring its render-*.ts, reusing only the pure state/render logic. See skill §17/§18
|
|
11
11
|
// for the one open async-exception repro and the honest cross-cutting gap list.
|
|
12
|
+
// Bare side-effect import, deliberately NOT a re-export and deliberately not beside one of the
|
|
13
|
+
// same specifier: it registers the press behavior with the engine's host-behavior registry. Any
|
|
14
|
+
// other shape is dropped by Metro's inlineRequires in a release build — see register.ts.
|
|
15
|
+
import './register.js';
|
|
12
16
|
export { mount, unmount } from './render.js';
|
|
13
17
|
export { AppRegistry, setHostRegistrar } from './modules/app-registry/index.js';
|
|
14
18
|
export { View, Text, ActivityIndicator, Image, ImageBackground, InputAccessoryView, KeyboardAvoidingView, Switch, TextInput, Modal, SafeAreaView, RefreshControl, Pressable, TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, TouchableNativeFeedback, Button, ScrollView, ScrollViewStickyHeader, VirtualizedList, FlatList, VirtualizedSectionList, SectionList, } from './components/index.js';
|