@symbiote-native/svelte 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/bootstrap.js +1 -1
- package/build/class-value.js +1 -1
- package/build/components/RefreshControl.svelte +4 -2
- package/build/components/Text.svelte +11 -2
- package/build/components/View.svelte +3 -1
- package/build/components/activity-indicator/index.svelte +8 -2
- package/build/components/button.svelte +8 -3
- package/build/components/flat-list/index.svelte +46 -11
- package/build/components/image/image-logic.js +13 -3
- package/build/components/image-background/index.svelte +8 -2
- package/build/components/index.d.ts +2 -2
- package/build/components/input-accessory-view/index.svelte +4 -1
- package/build/components/keyboard-avoiding-view/index.svelte +34 -11
- package/build/components/modal/index.svelte +4 -1
- package/build/components/pressable/index.svelte +10 -3
- package/build/components/pressable/pressable-props.d.ts +1 -1
- package/build/components/scroll-view/index.svelte +54 -21
- package/build/components/scroll-view/sticky-header.svelte +49 -16
- package/build/components/section-list/index.svelte +6 -1
- package/build/components/switch/index.svelte +14 -3
- package/build/components/text-input/index.svelte +22 -6
- package/build/components/touchable-highlight/index.svelte +114 -15
- package/build/components/touchable-highlight/touchable-highlight-props.d.ts +2 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback.svelte +7 -2
- package/build/components/touchable-opacity/index.svelte +112 -56
- package/build/components/touchable-without-feedback/index.svelte +59 -4
- package/build/components/virtualized-list/index.svelte +233 -70
- package/build/components/virtualized-list/virtualized-list-props.js +8 -4
- package/build/components/virtualized-section-list/index.svelte +56 -9
- package/build/components/virtualized-section-list/index.svelte.d.ts +1 -1
- package/build/components/virtualized-section-list/virtualized-section-list-props.d.ts +5 -0
- package/build/create-portal/index.d.ts +22 -0
- package/build/create-portal/index.js +160 -0
- package/build/descriptor-to-svelte.js +15 -21
- package/build/dom-shim/index.d.ts +1 -0
- package/build/dom-shim/index.js +3 -0
- package/build/dom-shim/text.js +38 -1
- package/build/host-instance.js +7 -2
- package/build/index.d.ts +5 -3
- package/build/index.js +16 -11
- package/build/modules/animated/create-animated-component.d.ts +5 -0
- package/build/modules/animated/create-animated-component.js +131 -0
- package/build/modules/animated/index.d.ts +24 -14
- package/build/modules/animated/index.js +30 -30
- package/build/modules/status-bar/index.js +1 -1
- package/build/modules/status-bar/index.svelte +4 -1
- package/build/native-view-bridge.d.ts +1 -1
- package/build/native-view-bridge.js +1 -1
- package/build/preprocessor/collapse-text-whitespace.d.ts +10 -0
- package/build/preprocessor/collapse-text-whitespace.js +138 -0
- package/build/preprocessor/forbid-web-only-constructs.js +8 -2
- package/build/preprocessor/scoped-styles.js +141 -104
- package/build/render.js +48 -3
- package/build/scope-token.d.ts +3 -2
- package/build/scope-token.js +6 -17
- package/build/style-scope.d.ts +2 -1
- package/build/style-scope.js +10 -8
- package/metro-svelte-transformer.cjs +17 -2
- package/package.json +11 -7
- package/build/modules/animated/AnimatedImage.svelte +0 -115
- package/build/modules/animated/AnimatedImage.svelte.d.ts +0 -5
- package/build/modules/animated/AnimatedScrollView.svelte +0 -92
- package/build/modules/animated/AnimatedScrollView.svelte.d.ts +0 -17
- package/build/modules/animated/AnimatedText.svelte +0 -58
- package/build/modules/animated/AnimatedText.svelte.d.ts +0 -5
- package/build/modules/animated/AnimatedView.svelte +0 -95
- package/build/modules/animated/AnimatedView.svelte.d.ts +0 -5
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
ISection,
|
|
14
14
|
} from './virtualized-section-list-props';
|
|
15
15
|
|
|
16
|
-
export type {
|
|
16
|
+
export type {
|
|
17
|
+
IVirtualizedSectionListProps,
|
|
18
|
+
IVirtualizedSectionListHandle,
|
|
19
|
+
ISection,
|
|
20
|
+
};
|
|
17
21
|
</script>
|
|
18
22
|
|
|
19
23
|
<script lang="ts" generics="ItemT">
|
|
@@ -30,7 +34,10 @@
|
|
|
30
34
|
import { Platform, dlog, type ISymbioteNode } from '@symbiote-native/engine';
|
|
31
35
|
import VirtualizedList from '../virtualized-list/index.svelte';
|
|
32
36
|
import { pickAccessibilityProps } from '../virtualized-list/virtualized-list-props';
|
|
33
|
-
import type {
|
|
37
|
+
import type {
|
|
38
|
+
IVirtualizedListHandle,
|
|
39
|
+
IScrollViewHandle,
|
|
40
|
+
} from '../virtualized-list/virtualized-list-props';
|
|
34
41
|
import type { IVirtualizedSectionListProps as IProps } from './virtualized-section-list-props';
|
|
35
42
|
import { pickAttachmentProps } from '../../runes/attachments';
|
|
36
43
|
|
|
@@ -50,7 +57,11 @@
|
|
|
50
57
|
|
|
51
58
|
// RN sticks section headers by default only on iOS; Android does not unless asked.
|
|
52
59
|
const stickyHeaderIndices = $derived(
|
|
53
|
-
resolveStickySectionHeaders(
|
|
60
|
+
resolveStickySectionHeaders(
|
|
61
|
+
props.stickySectionHeadersEnabled,
|
|
62
|
+
headerIndices,
|
|
63
|
+
Platform.OS,
|
|
64
|
+
),
|
|
54
65
|
);
|
|
55
66
|
// DIAGNOSTIC (2026-08-13, tracking why stickyHeaderIndices comes back undefined on Android
|
|
56
67
|
// despite the demo passing the bare `stickySectionHeadersEnabled` shorthand): the previous
|
|
@@ -78,11 +89,35 @@
|
|
|
78
89
|
function getEntryCount(): number {
|
|
79
90
|
return entries.length;
|
|
80
91
|
}
|
|
81
|
-
function entryKeyExtractor(
|
|
92
|
+
function entryKeyExtractor(
|
|
93
|
+
entry: ISectionEntry<ItemT>,
|
|
94
|
+
index: number,
|
|
95
|
+
): string {
|
|
82
96
|
return sectionEntryKey(entry, index, props.keyExtractor);
|
|
83
97
|
}
|
|
84
98
|
|
|
85
|
-
|
|
99
|
+
// Hand the callback `sections`, not the entries: RN's inner VirtualizedList gets
|
|
100
|
+
// `data={this.props.sections}` (VirtualizedSectionList.js:216) while ours streams the FLATTENED
|
|
101
|
+
// entries, so the same user code would otherwise see a different argument here than on RN.
|
|
102
|
+
//
|
|
103
|
+
// UPSTREAM-DIVERGENCE(react-native): the flat INDEX matches RN's (two rows per section, header
|
|
104
|
+
// and footer) only while the `sectionSeparator` snippet is unset. With it, flattenSections emits
|
|
105
|
+
// an extra 'section-separator' row per boundary that RN renders inside the neighbouring cell, so
|
|
106
|
+
// indices shift by one per boundary from the second section on. Deliberate - that row is how this
|
|
107
|
+
// adapter paints the separator; a caller combining the two must account for it.
|
|
108
|
+
const entryItemLayout = $derived.by(() => {
|
|
109
|
+
const getItemLayout = props.getItemLayout;
|
|
110
|
+
if (getItemLayout === undefined) return undefined;
|
|
111
|
+
return (
|
|
112
|
+
_entries: unknown,
|
|
113
|
+
index: number,
|
|
114
|
+
): { length: number; offset: number; index: number } =>
|
|
115
|
+
getItemLayout(props.sections, index);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
function entrySeparatorProps(
|
|
119
|
+
entryProps: ISeparatorProps<ISectionEntry<ItemT>>,
|
|
120
|
+
): ISeparatorProps<ItemT> {
|
|
86
121
|
return {
|
|
87
122
|
...entryProps,
|
|
88
123
|
leadingItem: unwrapEntryItem(entryProps.leadingItem),
|
|
@@ -100,9 +135,15 @@
|
|
|
100
135
|
viewPosition?: number;
|
|
101
136
|
animated?: boolean;
|
|
102
137
|
}): void {
|
|
103
|
-
const flatIndex = scrollLocationToFlatIndex(
|
|
138
|
+
const flatIndex = scrollLocationToFlatIndex(
|
|
139
|
+
headerIndices,
|
|
140
|
+
params.sectionIndex,
|
|
141
|
+
params.itemIndex,
|
|
142
|
+
);
|
|
104
143
|
if (flatIndex === undefined) {
|
|
105
|
-
dlog(
|
|
144
|
+
dlog(
|
|
145
|
+
`VirtualizedSectionList scrollToLocation: section ${params.sectionIndex} out of range`,
|
|
146
|
+
);
|
|
106
147
|
return;
|
|
107
148
|
}
|
|
108
149
|
dlog(
|
|
@@ -155,7 +196,12 @@
|
|
|
155
196
|
{:else if entry.kind === 'section-separator'}
|
|
156
197
|
{@render props.sectionSeparator?.()}
|
|
157
198
|
{:else}
|
|
158
|
-
{@render props.item({
|
|
199
|
+
{@render props.item({
|
|
200
|
+
item: entry.item,
|
|
201
|
+
index: entry.itemIndex,
|
|
202
|
+
section: entry.section,
|
|
203
|
+
separators,
|
|
204
|
+
})}
|
|
159
205
|
{/if}
|
|
160
206
|
{/snippet}
|
|
161
207
|
|
|
@@ -176,7 +222,8 @@
|
|
|
176
222
|
footer={props.footer}
|
|
177
223
|
empty={props.empty}
|
|
178
224
|
keyExtractor={entryKeyExtractor}
|
|
179
|
-
|
|
225
|
+
getItemLayout={entryItemLayout}
|
|
226
|
+
{stickyHeaderIndices}
|
|
180
227
|
inverted={props.inverted}
|
|
181
228
|
extraData={props.extraData}
|
|
182
229
|
onEndReached={props.onEndReached}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IVirtualizedSectionListProps, IVirtualizedSectionListHandle, ISection } from './virtualized-section-list-props';
|
|
2
|
-
export type { IVirtualizedSectionListProps, IVirtualizedSectionListHandle, ISection };
|
|
2
|
+
export type { IVirtualizedSectionListProps, IVirtualizedSectionListHandle, ISection, };
|
|
3
3
|
import { type ISymbioteNode } from '@symbiote-native/engine';
|
|
4
4
|
import type { IScrollViewHandle } from '../virtualized-list/virtualized-list-props';
|
|
5
5
|
import type { IVirtualizedSectionListProps as IProps } from './virtualized-section-list-props';
|
|
@@ -25,6 +25,11 @@ export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps
|
|
|
25
25
|
footer?: Snippet;
|
|
26
26
|
empty?: Snippet;
|
|
27
27
|
keyExtractor?: (item: ItemT, index: number) => string;
|
|
28
|
+
getItemLayout?: (data: ReadonlyArray<ISection<ItemT>> | null, index: number) => {
|
|
29
|
+
length: number;
|
|
30
|
+
offset: number;
|
|
31
|
+
index: number;
|
|
32
|
+
};
|
|
28
33
|
stickySectionHeadersEnabled?: boolean;
|
|
29
34
|
extraData?: unknown;
|
|
30
35
|
onEndReached?: (info: {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
import { SymbioteSurface, type ISymbioteNode } from '@symbiote-native/engine';
|
|
3
|
+
import { ShimElement } from '../dom-shim';
|
|
4
|
+
/**
|
|
5
|
+
* Where portaled content lands. React's `IPortalContainer` carries two members
|
|
6
|
+
* (`ISymbioteNode | SymbioteSurface`); this carries three, because Svelte's OWN handle on a
|
|
7
|
+
* mounted host node is the `ShimElement` a `bind:this` / `{@attach}` hands back — the same
|
|
8
|
+
* capability, one wrapper further out. `ISymbioteNode` stays accepted so a raw engine node
|
|
9
|
+
* (what `hostInstance()` returns, and what a third-party native-view package holds) is a target
|
|
10
|
+
* here exactly as it is on React.
|
|
11
|
+
*/
|
|
12
|
+
export type IPortalTarget = ShimElement | ISymbioteNode | SymbioteSurface;
|
|
13
|
+
export interface IPortalProps {
|
|
14
|
+
/**
|
|
15
|
+
* An already-mounted node in this surface, or the surface itself. Reactive: pointing it at a
|
|
16
|
+
* different target MOVES the content, it does not re-create it.
|
|
17
|
+
*/
|
|
18
|
+
mount: IPortalTarget;
|
|
19
|
+
/** The content to relocate. `<Portal mount={x}>…</Portal>` fills this in implicitly. */
|
|
20
|
+
children: Snippet;
|
|
21
|
+
}
|
|
22
|
+
export declare const Portal: Component<IPortalProps>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Portal — the Svelte adapter's same-surface portal: the twin of React's `createPortal`
|
|
2
|
+
// (adapters/react/src/create-portal/index.ts), Solid's `<Portal mount={…}>`
|
|
3
|
+
// (adapters/solid/src/create-portal/index.tsx) and Angular's PortalDirective/PortalOutletDirective
|
|
4
|
+
// pair. This adapter shipped `createTunnel` and no portal in any spelling until 2026-08-20; the
|
|
5
|
+
// two do NOT overlap (see the table at the bottom of this header), so the tunnel was never a
|
|
6
|
+
// substitute.
|
|
7
|
+
//
|
|
8
|
+
// WHY IT IS A COMPONENT (`<Portal mount={…}>`) AND NOT A CALL (`createPortal(content, target)`).
|
|
9
|
+
// NOT for Solid's reason. Solid had to avoid the call form because Solid evaluates JSX eagerly at
|
|
10
|
+
// the position it is written, so a call would BUILD the content before anything could relocate it.
|
|
11
|
+
// That argument does not transfer: probed against the installed svelte 5.56.8, `{#snippet
|
|
12
|
+
// children()}…{/snippet}` compiles to `const children = ($$anchor) => {…}` — a lazy closure that
|
|
13
|
+
// runs only when something calls it with an anchor, exactly like React's `children`. Svelte's
|
|
14
|
+
// reason is simpler and harder: **Svelte has no expression form for markup at all.** There is no
|
|
15
|
+
// `h()`, no JSX value; markup exists only inside a template, and the only way to hand a block of
|
|
16
|
+
// template to something else is a snippet prop. A `createPortal(snippet, target)` called from
|
|
17
|
+
// `<script>` would still have to be given a snippet — and would then need to invent its own
|
|
18
|
+
// lifetime, teardown and reactive ownership, all of which a component gets from the framework.
|
|
19
|
+
// `<Portal mount={node}>` is also the shape solid-js/web ships and the shape every community
|
|
20
|
+
// Svelte portal action ends up approximating, so a Svelte author already reads it.
|
|
21
|
+
//
|
|
22
|
+
// WHY THE BODY IS HAND-WRITTEN TS AND NOT A `.svelte` FILE — forced, not preferred. A `.svelte`
|
|
23
|
+
// template cannot express this component at all: `{@render children()}` always renders at the
|
|
24
|
+
// component's OWN anchor position, and the template language has no "render into node X" form.
|
|
25
|
+
// Choosing the destination means calling the snippet with an anchor of our own, which is
|
|
26
|
+
// precisely what the compiler's own `{@render}` does. Probed:
|
|
27
|
+
//
|
|
28
|
+
// {@render children()} -> $.snippet(node, () => $$props.children)
|
|
29
|
+
//
|
|
30
|
+
// So this file makes the SAME call the compiler emits and substitutes ONE argument: the anchor.
|
|
31
|
+
// The precedent for a hand-written component body in this adapter is
|
|
32
|
+
// modules/animated/create-animated-component.ts (see its header, and svelte-internal-client.d.ts
|
|
33
|
+
// for the narrowed declarations); the adapter is by design coupled to Svelte's private internals
|
|
34
|
+
// — the whole DOM shim is (svelte-adapter-dom-shim skill §0). A welcome side effect: `Portal` is
|
|
35
|
+
// plain TS, so it imports from ordinary TS and from vitest with no Svelte plugin in the way.
|
|
36
|
+
//
|
|
37
|
+
// SCOPE — same-surface only, matching React's boundary exactly, neither widened nor narrowed.
|
|
38
|
+
// `mount` must be an already-mounted node WITHIN THE SAME SURFACE as the Portal's call site
|
|
39
|
+
// (typically a host element you hold via `bind:this` or `{@attach}`), or that surface itself. It
|
|
40
|
+
// is not a route into a second, independently mount()ed surface: React's `resetAfterCommit` fires
|
|
41
|
+
// only for the primary root's own container, and here the equivalent is that a foreign surface
|
|
42
|
+
// would never be told to re-commit — a silent no-paint, not a crash. Cross-surface content
|
|
43
|
+
// sharing is a different mechanism: `createTunnel` (../create-tunnel).
|
|
44
|
+
//
|
|
45
|
+
// PORTAL vs TUNNEL — every row is pinned by a test, in this file's suite and the tunnel's:
|
|
46
|
+
//
|
|
47
|
+
// Reach | same surface only | any surface, incl. a separately mount()ed one
|
|
48
|
+
// Target must | no — any mounted node, | yes — a <TunnelOut/> must be rendered there
|
|
49
|
+
// cooperate | incl. one you hold a ref |
|
|
50
|
+
// Placement | the target's exact slot, | one collection point, registration order
|
|
51
|
+
// | interleaved with its own |
|
|
52
|
+
// | children |
|
|
53
|
+
// Content's | the CALL SITE (getContext | the OUT SITE (getContext resolves there)
|
|
54
|
+
// reactive owner | resolves there) |
|
|
55
|
+
// Node identity | nodes are MOVED (the old | content is re-created per TunnelOut
|
|
56
|
+
// | parent empties) |
|
|
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';
|
|
59
|
+
import { ShimComment, ShimElement, ShimNode } from '../dom-shim/index.js';
|
|
60
|
+
// The Svelte flavour of React's `isSymbioteNode` guard, and it needs its own wording: React tells
|
|
61
|
+
// the caller to check for a forgotten `.current`, which does not exist here. A Svelte host ref is
|
|
62
|
+
// populated by an EFFECT, so it is still `null` while the template that reads it first runs —
|
|
63
|
+
// gating the Portal behind `{#if target}` is the idiomatic fix (the twin of React's callback-ref
|
|
64
|
+
// gotcha and Solid's `<Show when={…}>`).
|
|
65
|
+
function assertPortalTarget(target) {
|
|
66
|
+
if (target instanceof ShimElement ||
|
|
67
|
+
target instanceof SymbioteSurface ||
|
|
68
|
+
isSymbioteNode(target)) {
|
|
69
|
+
return target;
|
|
70
|
+
}
|
|
71
|
+
throw new Error('Portal `mount` must be an already-mounted host node (a `bind:this` / `{@attach}` ref off a rendered component) or a surface — got something else. Is the ref still null because the template ran before the effect that fills it (gate the Portal behind `{#if target}`), or did you pass a CSS-selector-style string?');
|
|
72
|
+
}
|
|
73
|
+
function targetLabel(target) {
|
|
74
|
+
if (target instanceof SymbioteSurface)
|
|
75
|
+
return `surface#${target.rootTag}`;
|
|
76
|
+
if (target instanceof ShimElement)
|
|
77
|
+
return target.tagName;
|
|
78
|
+
return target.component;
|
|
79
|
+
}
|
|
80
|
+
// Attach the fragment host under `target` and hand back the matching detach. Three branches
|
|
81
|
+
// because the three target kinds sit at different layers:
|
|
82
|
+
//
|
|
83
|
+
// * ShimElement — attach in the SHIM tree and let it do the rest. Liveness is lazy there
|
|
84
|
+
// (shim-node.ts's makeLive), so this is the one branch that works whether or not the target
|
|
85
|
+
// has reached its first commit yet, and the host's surface arrives with it.
|
|
86
|
+
// * SymbioteSurface — no shim node exists above it, so the host is made live against the
|
|
87
|
+
// surface directly and appended as a top-level child, the same branch React's
|
|
88
|
+
// `appendChildToContainer` takes for `isSurfaceContainer`.
|
|
89
|
+
// * ISymbioteNode — a raw engine node carries no surface back-pointer, so the surface comes
|
|
90
|
+
// from the Portal's OWN call-site anchor. That is correct by construction for the only
|
|
91
|
+
// supported case: same-surface targets.
|
|
92
|
+
function attachHost(target, host, callSiteAnchor) {
|
|
93
|
+
if (target instanceof ShimElement) {
|
|
94
|
+
target.appendChild(host);
|
|
95
|
+
return () => {
|
|
96
|
+
target.removeChild(host);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (target instanceof SymbioteSurface) {
|
|
100
|
+
const node = host.makeLive(target);
|
|
101
|
+
target.appendChild(node);
|
|
102
|
+
target.requestCommit();
|
|
103
|
+
return () => {
|
|
104
|
+
target.removeChild(node);
|
|
105
|
+
target.requestCommit();
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const surface = callSiteAnchor?.surface;
|
|
109
|
+
if (surface === undefined) {
|
|
110
|
+
throw new Error('Portal `mount` was given a raw engine node, but the Portal itself is not mounted on a surface yet, so there is nothing to commit it to. Pass the `bind:this` / `{@attach}` value itself (a host element) rather than unwrapping it, or pass the surface.');
|
|
111
|
+
}
|
|
112
|
+
const node = host.makeLive(surface);
|
|
113
|
+
engineAppendChild(target, node);
|
|
114
|
+
surface.requestCommit();
|
|
115
|
+
return () => {
|
|
116
|
+
engineRemoveChild(target, node);
|
|
117
|
+
surface.requestCommit();
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export const Portal = function Portal(internals, props) {
|
|
121
|
+
// The compiled twin of a component's own `$props()` scope. Without it `user_effect` below is
|
|
122
|
+
// created immediately instead of being deferred to mount, and would run before the call site
|
|
123
|
+
// has finished rendering — the same reason create-animated-component.ts opens one.
|
|
124
|
+
push(props, true);
|
|
125
|
+
// ONE engine anchor per Portal instance, as a fragment host. It is a real retained node, so
|
|
126
|
+
// the content has a stable exclusive parent to be reconciled under, and the commit walk
|
|
127
|
+
// FLATTENS an anchor's children into its parent (renderableChildren, core/engine/src/commit.ts)
|
|
128
|
+
// so the anchor itself never paints. That is what keeps portaled content a DIRECT Fabric child
|
|
129
|
+
// of the target — matching React's createPortal, and unlike a DOM portal, which always leaves
|
|
130
|
+
// its container element in the tree.
|
|
131
|
+
//
|
|
132
|
+
// It is also what makes relocation and teardown one call each: the whole subtree travels with
|
|
133
|
+
// the anchor, so nothing has to track which nodes the snippet produced.
|
|
134
|
+
const host = new ShimComment('symbiote-portal');
|
|
135
|
+
// The anchor `$.snippet` renders BEFORE — a child of the host, so the content lands inside the
|
|
136
|
+
// host rather than wherever the host currently sits. Also an engine anchor: flattened away too.
|
|
137
|
+
const renderAnchor = new ShimComment('');
|
|
138
|
+
host.appendChild(renderAnchor);
|
|
139
|
+
// A component's first argument IS its anchor node, which in this adapter is a shim node — so
|
|
140
|
+
// the Portal can read its own surface off it. Guarded rather than cast: `ComponentInternals` is
|
|
141
|
+
// an opaque branded type, and a future Svelte could hand something else.
|
|
142
|
+
const callSiteAnchor = internals instanceof ShimNode ? internals : undefined;
|
|
143
|
+
// Render FIRST, relocate second — that order is the whole reason context, error boundaries and
|
|
144
|
+
// ownership resolve from the call site: the snippet runs inside THIS component's context, and
|
|
145
|
+
// `<Portal>` is written at the call site. Only host nodes move afterwards.
|
|
146
|
+
snippet(renderAnchor, () => props.children);
|
|
147
|
+
// `props.mount` is read inside the effect, not destructured at the top: a prop arrives as a
|
|
148
|
+
// getter (probed — the compiler emits `get mount() { return $.get(x); }`), so reading it here
|
|
149
|
+
// would freeze the first target. Returning the detach as the effect's cleanup covers both
|
|
150
|
+
// cases at once — a `mount` change (fired before the effect re-attaches elsewhere) and the
|
|
151
|
+
// Portal's own teardown.
|
|
152
|
+
user_effect(() => {
|
|
153
|
+
const target = assertPortalTarget(props.mount);
|
|
154
|
+
dlog(`svelte portal -> ${targetLabel(target)}`);
|
|
155
|
+
return attachHost(target, host, callSiteAnchor);
|
|
156
|
+
});
|
|
157
|
+
// Nothing paints at the call site — React's createPortal returns a ReactPortal that renders
|
|
158
|
+
// nothing there for the same reason. A component's return value is its exports; there are none.
|
|
159
|
+
return pop({});
|
|
160
|
+
};
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
// function's actual output changed shape, which this bridge's whole cost-free model assumes
|
|
20
20
|
// never happens. If this ever fires for real, the render fn genuinely stopped being
|
|
21
21
|
// shape-stable and needs its own fix, not a workaround here.
|
|
22
|
+
import { createDescriptorShapeGuard } from '@symbiote-native/components';
|
|
22
23
|
import { getShimDocument } from './dom-shim/index.js';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
24
|
+
// The predicates live in @symbiote-native/components, next to the Descriptor whose contract they
|
|
25
|
+
// guard (skill §15/§19 for why this bridge depends on it at all). Solid's bridge had grown a
|
|
26
|
+
// private copy of the same checks with different coverage; one owner ends that.
|
|
27
|
+
const shape = createDescriptorShapeGuard('descriptorToSvelte');
|
|
28
28
|
function buildChild(child) {
|
|
29
29
|
const document = getShimDocument();
|
|
30
30
|
if (typeof child === 'string') {
|
|
@@ -40,22 +40,17 @@ function buildChild(child) {
|
|
|
40
40
|
return { kind: 'element', shim, children };
|
|
41
41
|
}
|
|
42
42
|
function syncChild(cached, child) {
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
cached.shim.data = child;
|
|
43
|
+
if (cached.kind === 'text') {
|
|
44
|
+
const text = shape.asText(child);
|
|
45
|
+
if (cached.shim.data !== text)
|
|
46
|
+
cached.shim.data = text;
|
|
48
47
|
return;
|
|
49
48
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
cached.
|
|
55
|
-
if (cached.children.length !== child.children.length) {
|
|
56
|
-
throw new Error(shapeChangedMessage('child count'));
|
|
57
|
-
}
|
|
58
|
-
cached.children.forEach((c, index) => syncChild(c, child.children[index]));
|
|
49
|
+
const element = shape.asElement(child);
|
|
50
|
+
shape.assertType(cached.shim.tagName, element.type);
|
|
51
|
+
cached.shim.p = element.props;
|
|
52
|
+
shape.assertChildCount(cached.children.length, element.children.length);
|
|
53
|
+
cached.children.forEach((c, index) => syncChild(c, element.children[index]));
|
|
59
54
|
}
|
|
60
55
|
// Materializes `descriptor.children` onto an already-live `parent` ONCE, then reuses the same
|
|
61
56
|
// shim nodes by position on every `update()`. `parent` is expected to already be live (has an
|
|
@@ -70,8 +65,7 @@ export function mountDescriptorChildren(parent, children) {
|
|
|
70
65
|
});
|
|
71
66
|
return {
|
|
72
67
|
update(next) {
|
|
73
|
-
|
|
74
|
-
throw new Error(shapeChangedMessage('root child count'));
|
|
68
|
+
shape.assertChildCount(cached.length, next.length);
|
|
75
69
|
cached.forEach((c, index) => syncChild(c, next[index]));
|
|
76
70
|
},
|
|
77
71
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { patchGlobals, restoreGlobals } from './patch-globals';
|
|
2
2
|
export { ShimElement } from './element';
|
|
3
|
+
export { ShimComment } from './comment';
|
|
3
4
|
export { ShimNode } from './shim-node';
|
|
4
5
|
export { ShimText } from './text';
|
|
5
6
|
export { getShimDocument } from './document';
|
package/build/dom-shim/index.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// never sees a shim class or a `symbiote-*` tag; see the adapter's own index.ts).
|
|
3
3
|
export { patchGlobals, restoreGlobals } from './patch-globals.js';
|
|
4
4
|
export { ShimElement } from './element.js';
|
|
5
|
+
// ShimComment is the anchor path (engine createAnchor, flattened away by the commit walk) — the
|
|
6
|
+
// only shim node create-portal can use as a non-painting fragment host.
|
|
7
|
+
export { ShimComment } from './comment.js';
|
|
5
8
|
export { ShimNode } from './shim-node.js';
|
|
6
9
|
export { ShimText } from './text.js';
|
|
7
10
|
export { getShimDocument } from './document.js';
|
package/build/dom-shim/text.js
CHANGED
|
@@ -18,6 +18,39 @@
|
|
|
18
18
|
// native-node-parity.test.ts, which diffs committed native trees against the Vue adapter.
|
|
19
19
|
import { 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
|
+
// Deliberately WIDER than Svelte's own whitespace class. `svelte/src/compiler/phases/patterns.js`
|
|
22
|
+
// uses /[^ \t\r\n]/ and says why: "Not \S because that also removes explicit whitespace defined
|
|
23
|
+
// through things like ` `". For Svelte the character IS the discriminator, so it must keep
|
|
24
|
+
// an author's deliberate nbsp. For us the PARENT is the discriminator, and it has already proved
|
|
25
|
+
// the node is unrenderable - a raw text under a non-text parent cannot paint whatever character
|
|
26
|
+
// it holds. So ` `, ` `, \f, \v, U+2028, U+3000 and the zero-width family (which `\s`
|
|
27
|
+
// misses) all drop here, while a deliberate nbsp INSIDE a <Text> is kept by the parent check.
|
|
28
|
+
// Measured: each of these arrives as its own text node in the from_tree template.
|
|
29
|
+
const WHITESPACE_ONLY = /^[\s\u200b-\u200d\ufeff]+$/;
|
|
30
|
+
// Whitespace-only text under a parent that cannot hold raw text is FORMATTING, not content: the
|
|
31
|
+
// gap Svelte leaves between two sibling tags written on separate lines. Svelte collapses every
|
|
32
|
+
// such run to a single ' ' but never deletes it — in the DOM that space separates inline words
|
|
33
|
+
// and only CSS decides whether it paints. Fabric has no such layer, so a raw text outside a
|
|
34
|
+
// <Text> is simply invalid: the same invariant the engine enforces at commit time.
|
|
35
|
+
//
|
|
36
|
+
// The PARENT is what makes this exact rather than a heuristic. Measured on svelte 5.56.8, a
|
|
37
|
+
// stray gap and an {#each} text placeholder are the same ' ' string in the from_tree template:
|
|
38
|
+
//
|
|
39
|
+
// stray gap ['symbiote-view', null, [...], ' ', [...]] parent takes no raw text -> drop
|
|
40
|
+
// placeholder ['symbiote-text', null, ' '] parent IS a <Text> -> keep
|
|
41
|
+
//
|
|
42
|
+
// So `<Text><Text>a</Text> <Text>b</Text></Text>` keeps its separator, correctly — there the
|
|
43
|
+
// space really is a word boundary. This also covers the one shape the source preprocessor
|
|
44
|
+
// admits it cannot catch (two siblings, one line, no newline): by here Svelte has already
|
|
45
|
+
// normalized that form to the very same single space.
|
|
46
|
+
//
|
|
47
|
+
// makeLive() binds a parent before its children, so the parent's engine node is always present
|
|
48
|
+
// by the time this runs; a fragment is unwrapped into the real parent before insertion.
|
|
49
|
+
function isFormattingWhitespace(value, parent) {
|
|
50
|
+
if (!WHITESPACE_ONLY.test(value))
|
|
51
|
+
return false;
|
|
52
|
+
return parent?.engineNode?.isText !== true;
|
|
53
|
+
}
|
|
21
54
|
export class ShimText extends ShimNode {
|
|
22
55
|
value;
|
|
23
56
|
constructor(value) {
|
|
@@ -92,6 +125,10 @@ export class ShimText extends ShimNode {
|
|
|
92
125
|
return new ShimText(this.value);
|
|
93
126
|
}
|
|
94
127
|
createEngineNode() {
|
|
95
|
-
|
|
128
|
+
if (this.value === '')
|
|
129
|
+
return createAnchor();
|
|
130
|
+
if (isFormattingWhitespace(this.value, this.parent))
|
|
131
|
+
return createAnchor();
|
|
132
|
+
return createRawText(this.value);
|
|
96
133
|
}
|
|
97
134
|
}
|
package/build/host-instance.js
CHANGED
|
@@ -15,7 +15,10 @@ import { getNativeTag, isSymbioteNode, toPublicInstance, dlog, } from '@symbiote
|
|
|
15
15
|
// and none of those is a host ref an interop library can hand back. `tagName` is ShimElement's
|
|
16
16
|
// own field, so checking it makes the predicate mean what its name says.
|
|
17
17
|
function isShimElement(value) {
|
|
18
|
-
return typeof value === 'object' &&
|
|
18
|
+
return (typeof value === 'object' &&
|
|
19
|
+
value !== null &&
|
|
20
|
+
'engineNode' in value &&
|
|
21
|
+
'tagName' in value);
|
|
19
22
|
}
|
|
20
23
|
// The typed imperative handle (measure/measureInWindow/measureLayout/setNativeProps/focus/blur)
|
|
21
24
|
// a `bind:this` host ref carries — the Svelte twin of a React `ref.current`/Vue template ref
|
|
@@ -28,7 +31,9 @@ export function hostInstance(shim) {
|
|
|
28
31
|
if (shim === null || shim === undefined)
|
|
29
32
|
return undefined;
|
|
30
33
|
const node = shim.engineNode;
|
|
31
|
-
return node !== undefined && isSymbioteNode(node)
|
|
34
|
+
return node !== undefined && isSymbioteNode(node)
|
|
35
|
+
? toPublicInstance(node)
|
|
36
|
+
: undefined;
|
|
32
37
|
}
|
|
33
38
|
export function findNodeHandle(componentOrHandle) {
|
|
34
39
|
if (componentOrHandle === null || componentOrHandle === undefined)
|
package/build/index.d.ts
CHANGED
|
@@ -17,12 +17,14 @@ export type { IAccessibilityChangeEvent, IAccessibilityChangeEventName, IAccessi
|
|
|
17
17
|
export { findNodeHandle, hostInstance } from './host-instance';
|
|
18
18
|
export type { IHostInstance } from './host-instance';
|
|
19
19
|
export type { ShimElement } from './dom-shim';
|
|
20
|
-
export { createTunnel, TunnelIn, TunnelOut, type ITunnel } from './create-tunnel';
|
|
20
|
+
export { createTunnel, TunnelIn, TunnelOut, type ITunnel, } from './create-tunnel';
|
|
21
|
+
export { Portal, type IPortalProps, type IPortalTarget } from './create-portal';
|
|
21
22
|
export { useWindowDimensions } from './runes/use-window-dimensions.svelte';
|
|
22
23
|
export { useColorScheme } from './runes/use-color-scheme.svelte';
|
|
23
|
-
export { innerWidth, innerHeight, outerWidth, outerHeight, devicePixelRatio } from './runes/window';
|
|
24
|
+
export { innerWidth, innerHeight, outerWidth, outerHeight, devicePixelRatio, } from './runes/window';
|
|
24
25
|
export { orientation, createWidthQuery } from './runes/media-query';
|
|
25
26
|
export type { IOrientation, IWidthQueryBounds } from './runes/media-query';
|
|
26
27
|
export type { IReactiveValue } from './runes/dimensions-value';
|
|
27
28
|
export { normalizeSvelteClass, resolveSvelteClass, type ISvelteClassValue, type IClassMap, type IClassEntry, } from './class-value';
|
|
28
|
-
export { Animated } from './modules/animated';
|
|
29
|
+
export { Animated, createAnimatedComponent } from './modules/animated';
|
|
30
|
+
export type { IAnimatedComponentProps } from './modules/animated';
|
package/build/index.js
CHANGED
|
@@ -46,11 +46,17 @@ export { findNodeHandle, hostInstance } from './host-instance.js';
|
|
|
46
46
|
// (mount/unmount, for a downstream package's own tests).
|
|
47
47
|
// createTunnel: the Svelte twin of adapters/vue/src/create-tunnel (see create-tunnel/tunnel.ts's
|
|
48
48
|
// header for why the API shape — an explicit `tunnel` prop on TunnelIn/TunnelOut, rather than
|
|
49
|
-
// `tunnel.In`/`tunnel.Out` — deliberately differs from React/Vue).
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
49
|
+
// `tunnel.In`/`tunnel.Out` — deliberately differs from React/Vue).
|
|
50
|
+
export { createTunnel, TunnelIn, TunnelOut, } from './create-tunnel/index.js';
|
|
51
|
+
// Portal: the Svelte twin of React's createPortal / Solid's <Portal mount={…}> / Angular's
|
|
52
|
+
// PortalDirective. It is NOT interchangeable with createTunnel above — the two capabilities do
|
|
53
|
+
// not overlap (create-portal/index.ts's header carries the row-by-row table, each row pinned by a
|
|
54
|
+
// test). Portal uniquely buys a target that renders no outlet — the only route to a container you
|
|
55
|
+
// merely hold a ref to — and call-site `getContext`; the tunnel uniquely buys cross-surface reach.
|
|
56
|
+
// (This barrel used to state that createPortal "has no Svelte twin ... with no equivalent in a
|
|
57
|
+
// framework with no reconciler". That was wrong: a portal needs a retained tree you can move a
|
|
58
|
+
// subtree within, not a reconciler, and this adapter has one.)
|
|
59
|
+
export { Portal } from './create-portal/index.js';
|
|
54
60
|
// useWindowDimensions / useColorScheme: the Svelte twins of React's hooks / Vue's composables —
|
|
55
61
|
// see runes/use-window-dimensions.svelte.ts's header for why this adapter's lifecycle-helper
|
|
56
62
|
// bucket is named `runes/`, not `hooks/`/`composables/`.
|
|
@@ -60,7 +66,7 @@ export { useColorScheme } from './runes/use-color-scheme.svelte';
|
|
|
60
66
|
// `undefined` forever here. Same names, same `.current` shape, engine Dimensions/PixelRatio
|
|
61
67
|
// underneath — see runes/window.ts's header for the two deliberate differences from upstream, and
|
|
62
68
|
// for why `scrollX`/`scrollY`/`screenLeft`/`screenTop` are absent rather than faked.
|
|
63
|
-
export { innerWidth, innerHeight, outerWidth, outerHeight, devicePixelRatio } from './runes/window.js';
|
|
69
|
+
export { innerWidth, innerHeight, outerWidth, outerHeight, devicePixelRatio, } from './runes/window.js';
|
|
64
70
|
// What replaces `MediaQuery` from `svelte/reactivity`: named exports for the media features RN can
|
|
65
71
|
// actually answer, instead of a class taking a CSS query string it would mostly have to answer
|
|
66
72
|
// `false` to. `(prefers-color-scheme)`'s twin is `useColorScheme()` above. See
|
|
@@ -70,8 +76,7 @@ export { orientation, createWidthQuery } from './runes/media-query.js';
|
|
|
70
76
|
// IClassNameValue — plus the normalizer, for a consumer building a class value in its own
|
|
71
77
|
// helper. See class-value.ts's header for the object-is-a-style vs object-is-a-clsx-map rule.
|
|
72
78
|
export { normalizeSvelteClass, resolveSvelteClass, } from './class-value.js';
|
|
73
|
-
// Animated: the Svelte twin of adapters/vue/src/modules/animated
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
export { Animated } from './modules/animated/index.js';
|
|
79
|
+
// Animated: the Svelte twin of adapters/vue/src/modules/animated. All six RN animated components
|
|
80
|
+
// ship (View/Text/Image/ScrollView/FlatList/SectionList), all of them from the same generic
|
|
81
|
+
// createAnimatedComponent() wrap React, Vue and Solid also export.
|
|
82
|
+
export { Animated, createAnimatedComponent } from './modules/animated/index.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Component, ComponentInternals } from 'svelte';
|
|
2
|
+
import type { IAnimatedComponentProps } from './animated-component-props';
|
|
3
|
+
type IAnimatedBase<TExports extends Record<string, unknown>> = (internals: ComponentInternals, props: never) => TExports;
|
|
4
|
+
export declare function createAnimatedComponent<TExports extends Record<string, unknown>>(Base: IAnimatedBase<TExports>): Component<IAnimatedComponentProps, TExports>;
|
|
5
|
+
export {};
|