@symbiote-native/svelte 3.0.1 → 3.0.3
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 +64 -29
- package/build/components/flat-list/index.svelte +11 -0
- package/build/components/modal/index.svelte +24 -23
- package/build/components/section-list/index.svelte +2 -0
- package/build/components/virtualized-list/index.svelte +15 -1
- package/build/components/virtualized-list/virtualized-list-props.d.ts +2 -0
- package/build/components/virtualized-section-list/index.svelte +2 -0
- package/build/components/virtualized-section-list/virtualized-section-list-props.d.ts +2 -0
- package/build/dom-shim/canonical-prop-names.d.ts +1 -0
- package/build/dom-shim/canonical-prop-names.js +84 -0
- package/build/dom-shim/element.js +4 -2
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @symbiote-native/svelte
|
|
2
2
|
|
|
3
3
|
The **Svelte adapter** for [SymbioteNative](../../README.md) — render real native iOS/Android views
|
|
4
|
-
from Svelte 5, on the _same_ untouched core as React, Vue, and Angular, with React Native's own
|
|
4
|
+
from Svelte 5, on the _same_ untouched core as React, Vue, Solid, and Angular, with React Native's own
|
|
5
5
|
renderer never in the path. Svelte ships no official host-renderer API yet (`createRenderer` is
|
|
6
6
|
still an unmerged PR upstream), so instead of hooking a framework-blessed extension point this
|
|
7
7
|
adapter patches `globalThis`'s DOM classes: stock compiled Svelte client output believes it is
|
|
@@ -23,35 +23,84 @@ Svelte is the **sharpest test yet** that the core is genuinely framework-agnosti
|
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
26
|
+
```bash
|
|
27
|
+
npx @symbiote-native/cli new my-app --framework svelte
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
One command, nothing to wire by hand: every component here is a `.svelte` SFC with no TSX
|
|
31
|
+
alternative, so the Metro wiring below isn't optional — the generator sets it up, plus
|
|
32
|
+
`@symbiote-native/svelte`/`react-native`/`svelte` as your app's own dependencies.
|
|
33
|
+
|
|
34
|
+
<details>
|
|
35
|
+
<summary>Manual install (no generator — an existing app, or you want to wire it yourself)</summary>
|
|
36
|
+
|
|
26
37
|
```bash
|
|
27
38
|
npm install @symbiote-native/svelte react-native svelte
|
|
28
39
|
```
|
|
29
40
|
|
|
30
41
|
`react-native` and `svelte` stay your app's own top-level dependencies — this package only
|
|
31
|
-
replaces the JS renderer that drives them.
|
|
32
|
-
|
|
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:
|
|
42
|
+
replaces the JS renderer that drives them. Follow [`examples/svelte`](../../examples/svelte) for
|
|
43
|
+
both pieces of Metro wiring below; there is no wiring script for an existing app:
|
|
35
44
|
|
|
36
45
|
- `metro.config.js` — point `babelTransformerPath` at
|
|
37
46
|
`@symbiote-native/svelte/metro-svelte-transformer` (compiles `.svelte` on the way into the
|
|
38
47
|
bundle) and disable `inlineRequires` (see [the gotcha
|
|
39
48
|
below](#a-svelte-specific-gotcha--inlinerequires-vs-the-svelte-runtime)).
|
|
40
|
-
- `svelte.config.js` — registers the `forbidWebOnlyConstructs()
|
|
41
|
-
preprocessors, so `svelte-check` and the editor catch the same things
|
|
42
|
-
already guards against at build time
|
|
43
|
-
|
|
49
|
+
- `svelte.config.js` — registers the `forbidWebOnlyConstructs()`, `scopedStyles()`, and
|
|
50
|
+
`collapseTextWhitespace()` preprocessors, so `svelte-check` and the editor catch the same things
|
|
51
|
+
Metro's transformer already guards against at build time (the third one matters beyond
|
|
52
|
+
diagnostics: without it, a sentence wrapped across source lines for readability ships a literal
|
|
53
|
+
newline into the native text content — Svelte doesn't collapse it the way a browser or Vue's
|
|
54
|
+
compiler would). Also sets `compilerOptions: { fragments: 'tree', css: 'external' }` and a
|
|
55
|
+
`warningFilter` that silences `element_invalid_self_closing_tag` for `<pressable />`-style
|
|
56
|
+
self-closing primitives.
|
|
57
|
+
|
|
58
|
+
</details>
|
|
44
59
|
|
|
45
60
|
---
|
|
46
61
|
|
|
47
62
|
## Use it
|
|
48
63
|
|
|
64
|
+
The app is ordinary Svelte 5 — the native primitives are plain intrinsic tags, no import needed.
|
|
65
|
+
Styling is a CSS class against the component's own `<style>` block — the convention every example
|
|
66
|
+
app here follows. A tap→increment counter, using runes:
|
|
67
|
+
|
|
68
|
+
```svelte
|
|
69
|
+
<script lang="ts">
|
|
70
|
+
let count = $state(0);
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<safe-area-view class="screen">
|
|
74
|
+
<text>Taps: {count}</text>
|
|
75
|
+
<pressable onPress={() => count++}>
|
|
76
|
+
<text>Tap me</text>
|
|
77
|
+
</pressable>
|
|
78
|
+
</safe-area-view>
|
|
79
|
+
|
|
80
|
+
<style>
|
|
81
|
+
.screen {
|
|
82
|
+
flex: 1;
|
|
83
|
+
align-items: center;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
padding: 24px;
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
<details>
|
|
91
|
+
<summary>Native entry point (index.js) — already scaffolded by <code>npx @symbiote-native/cli new --framework svelte</code></summary>
|
|
92
|
+
|
|
49
93
|
The native entry reaches the _same_ seam as every other adapter. `createApp(App).mount(appName)`
|
|
50
94
|
wires the native-host seams and RN's own `AppRegistry`, then mounts via `@symbiote-native/engine` —
|
|
51
95
|
RN's own renderer is never in the path:
|
|
52
96
|
|
|
53
97
|
```js
|
|
54
98
|
// index.js
|
|
99
|
+
|
|
100
|
+
// Registers host behaviors (Image, Pressable, Switch, ...) that /bootstrap alone doesn't
|
|
101
|
+
// reach; deleting this breaks them silently (Metro's production inlineRequires makes a
|
|
102
|
+
// side-effect-only barrel import go lazy, see register.ts).
|
|
103
|
+
import '@symbiote-native/svelte';
|
|
55
104
|
import { createApp } from '@symbiote-native/svelte/bootstrap';
|
|
56
105
|
import App from './App.svelte';
|
|
57
106
|
import { name as appName } from './app.json';
|
|
@@ -59,21 +108,7 @@ import { name as appName } from './app.json';
|
|
|
59
108
|
createApp(App).mount(appName);
|
|
60
109
|
```
|
|
61
110
|
|
|
62
|
-
|
|
63
|
-
A tap→increment counter, using runes:
|
|
64
|
-
|
|
65
|
-
```svelte
|
|
66
|
-
<script lang="ts">
|
|
67
|
-
let count = $state(0);
|
|
68
|
-
</script>
|
|
69
|
-
|
|
70
|
-
<view style={{ padding: 24 }}>
|
|
71
|
-
<text>Taps: {count}</text>
|
|
72
|
-
<pressable onPress={() => count++}>
|
|
73
|
-
<text>Tap me</text>
|
|
74
|
-
</pressable>
|
|
75
|
-
</view>
|
|
76
|
-
```
|
|
111
|
+
</details>
|
|
77
112
|
|
|
78
113
|
The full canary is [`examples/svelte`](../../examples/svelte) — a stock RN 0.86 app whose
|
|
79
114
|
[`App.svelte`](../../examples/svelte/App.svelte) exercises the same surface as the React, Vue, and
|
|
@@ -84,8 +119,8 @@ Angular reference canaries.
|
|
|
84
119
|
## Parity — and the one gap
|
|
85
120
|
|
|
86
121
|
Svelte reaches the same primitives, runtime modules, `Animated` on both drivers, gestures,
|
|
87
|
-
accessibility, and the `VirtualizedList` family as React, Vue, and Angular, verified
|
|
88
|
-
iOS and Android. That parity is **structural, not hand-copied**: the component logic (state
|
|
122
|
+
accessibility, and the `VirtualizedList` family as React, Vue, Solid, and Angular, verified
|
|
123
|
+
on-device on iOS and Android. That parity is **structural, not hand-copied**: the component logic (state
|
|
89
124
|
machines + render functions) is written **once** in `@symbiote-native/components`, and Svelte
|
|
90
125
|
supplies only its lifecycle — runes (`$state` / `$derived` / `$effect`) driving hand-authored
|
|
91
126
|
`.svelte` markup that mirrors each `render-*.ts` directly, rather than a generic descriptor bridge
|
|
@@ -124,9 +159,9 @@ guarding it.
|
|
|
124
159
|
|
|
125
160
|
## Run it
|
|
126
161
|
|
|
127
|
-
[`examples/svelte`](../../examples/svelte) is a stock React Native 0.86 app. Requires Node ≥ 22
|
|
128
|
-
and the [RN environment
|
|
129
|
-
CocoaPods):
|
|
162
|
+
[`examples/svelte`](../../examples/svelte) is a stock React Native 0.86 app. Requires Node ≥ 22.13
|
|
163
|
+
(react-native 0.86's own `package.json#engines`) and the [RN environment
|
|
164
|
+
setup](https://reactnative.dev/docs/set-up-your-environment) (Xcode, CocoaPods):
|
|
130
165
|
|
|
131
166
|
```bash
|
|
132
167
|
cd examples/svelte
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
expandRowViewability,
|
|
18
18
|
firstItemOfRow,
|
|
19
19
|
lastItemOfRow,
|
|
20
|
+
removeClippedSubviewsOrDefault,
|
|
20
21
|
rowKeyExtractor,
|
|
21
22
|
type IRow,
|
|
22
23
|
type ISeparatorProps,
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
type IViewableItemsChangedInfo,
|
|
25
26
|
} from '@symbiote-native/components';
|
|
26
27
|
import {
|
|
28
|
+
Platform,
|
|
27
29
|
resolveClassName,
|
|
28
30
|
type ISymbioteNode,
|
|
29
31
|
} from '@symbiote-native/engine';
|
|
@@ -38,6 +40,11 @@
|
|
|
38
40
|
|
|
39
41
|
let props: IProps<ItemT> = $props();
|
|
40
42
|
|
|
43
|
+
// FlatList.js always sends it, defaulted per platform.
|
|
44
|
+
const removeClippedSubviews = $derived(
|
|
45
|
+
removeClippedSubviewsOrDefault(props.removeClippedSubviews, Platform.OS),
|
|
46
|
+
);
|
|
47
|
+
|
|
41
48
|
// $state.raw: the inner VirtualizedList component instance (its exports), held by identity —
|
|
42
49
|
// same concern as every other handle in this adapter (Switch's hostShim, VirtualizedList's own
|
|
43
50
|
// scrollHandle over its ShimElement).
|
|
@@ -238,6 +245,8 @@
|
|
|
238
245
|
scrollEventThrottle={props.scrollEventThrottle}
|
|
239
246
|
keyboardShouldPersistTaps={props.keyboardShouldPersistTaps}
|
|
240
247
|
keyboardDismissMode={props.keyboardDismissMode}
|
|
248
|
+
{removeClippedSubviews}
|
|
249
|
+
nestedScrollEnabled={props.nestedScrollEnabled}
|
|
241
250
|
style={props.style}
|
|
242
251
|
contentContainerStyle={props.contentContainerStyle}
|
|
243
252
|
class={props.class}
|
|
@@ -286,6 +295,8 @@
|
|
|
286
295
|
scrollEventThrottle={props.scrollEventThrottle}
|
|
287
296
|
keyboardShouldPersistTaps={props.keyboardShouldPersistTaps}
|
|
288
297
|
keyboardDismissMode={props.keyboardDismissMode}
|
|
298
|
+
{removeClippedSubviews}
|
|
299
|
+
nestedScrollEnabled={props.nestedScrollEnabled}
|
|
289
300
|
style={props.style}
|
|
290
301
|
contentContainerStyle={props.contentContainerStyle}
|
|
291
302
|
class={props.class}
|
|
@@ -6,9 +6,7 @@
|
|
|
6
6
|
// @symbiote-native/components and are shared verbatim with React/Vue (core/components/src/
|
|
7
7
|
// state/modal.ts + view/render-modal.ts); Svelte supplies only the lifecycle:
|
|
8
8
|
// - $state over the keep-alive reducer (switchReducer's twin: modalReducer)
|
|
9
|
-
// - a
|
|
10
|
-
// after the DOM update, the same "one keep-alive frame survives" timing as React's
|
|
11
|
-
// useEffect / Vue's flush:'post' watch)
|
|
9
|
+
// - a $effect arming the keep-alive on show, and the native dismiss dropping it (iOS)
|
|
12
10
|
// - the descriptor bridge: renderModal() always paints the SAME fixed shape (one
|
|
13
11
|
// modal host wrapping one view container — only prop VALUES vary,
|
|
14
12
|
// never structure, per svelte-adapter-dom-shim skill §15), so rather than building a
|
|
@@ -24,41 +22,42 @@
|
|
|
24
22
|
<script lang="ts">
|
|
25
23
|
import {
|
|
26
24
|
createInitialModalState,
|
|
25
|
+
isModalVisible,
|
|
27
26
|
modalReducer,
|
|
27
|
+
modalVisibilityAction,
|
|
28
28
|
renderModal,
|
|
29
29
|
resolveAccessibilityProps,
|
|
30
30
|
shouldRenderModal,
|
|
31
31
|
} from '@symbiote-native/components';
|
|
32
|
-
import { dlog } from '@symbiote-native/engine';
|
|
32
|
+
import { dlog, Platform } from '@symbiote-native/engine';
|
|
33
33
|
import { createAttachmentsSync } from '../../runes/attachments';
|
|
34
34
|
import type { ShimElement } from '../../dom-shim';
|
|
35
35
|
|
|
36
36
|
let rawProps: IModalProps = $props();
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// Not a $derived candidate despite the $state+$effect shape below: modalReducer folds over the
|
|
39
|
+
// PREVIOUS `state` (self-referential — $derived can't read the value it's replacing) and must
|
|
40
|
+
// run POST-render (see the effect's own comment) so the keep-alive frame survives a commit.
|
|
41
|
+
let localState = $state(createInitialModalState(rawProps.visible === true));
|
|
39
42
|
|
|
40
43
|
const resolved = $derived(resolveAccessibilityProps(rawProps));
|
|
41
|
-
const isVisible = $derived(resolved.visible
|
|
42
|
-
const shouldRender = $derived(shouldRenderModal(isVisible,
|
|
44
|
+
const isVisible = $derived(isModalVisible(resolved.visible));
|
|
45
|
+
const shouldRender = $derived(shouldRenderModal(isVisible, localState));
|
|
43
46
|
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
// more frame (state.isRendered still true) before the NEXT render drops it. The reducer is
|
|
47
|
-
// identity-stable, so a no-op transition — including this effect's own first run on mount —
|
|
48
|
-
// triggers no extra render.
|
|
49
|
-
//
|
|
50
|
-
// Under this adapter's microtask-coalesced requestCommit() (element.ts's `p` setter), Svelte
|
|
51
|
-
// settles the whole visible->hidden cascade within the SAME microtask flush that processes the
|
|
52
|
-
// triggering prop change, so the keep-alive frame genuinely happens (a render sees
|
|
53
|
-
// isVisible=false with isRendered still true before this effect flips it) but isn't observable
|
|
54
|
-
// as its own committed Fabric frame the way React's per-render synchronous commit makes it.
|
|
47
|
+
// Arms the iOS keep-alive on show; a hide is left to the native dismiss (state/modal.ts). The
|
|
48
|
+
// reducer is identity-stable, so the mount run triggers no extra render.
|
|
55
49
|
$effect(() => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
isVisible ? { type: 'show' } : { type: 'hide' },
|
|
59
|
-
);
|
|
50
|
+
const action = modalVisibilityAction(isVisible);
|
|
51
|
+
if (action !== undefined) localState = modalReducer(localState, action);
|
|
60
52
|
});
|
|
61
53
|
|
|
54
|
+
// Modal.js: onDismiss is iOS-only — it drops the keep-alive, then tells the app.
|
|
55
|
+
function handleDismiss(): void {
|
|
56
|
+
if (Platform.OS !== 'ios') return;
|
|
57
|
+
localState = modalReducer(localState, { type: 'hide' });
|
|
58
|
+
rawProps.onDismiss?.();
|
|
59
|
+
}
|
|
60
|
+
|
|
62
61
|
$effect(() => {
|
|
63
62
|
if (!shouldRender) dlog('Modal hidden -> no node committed');
|
|
64
63
|
});
|
|
@@ -82,8 +81,10 @@
|
|
|
82
81
|
style,
|
|
83
82
|
class: className,
|
|
84
83
|
children: _children,
|
|
85
|
-
|
|
84
|
+
onDismiss: _onDismiss,
|
|
85
|
+
...rest
|
|
86
86
|
} = resolved;
|
|
87
|
+
const passthrough = { ...rest, onDismiss: handleDismiss };
|
|
87
88
|
|
|
88
89
|
const descriptor = renderModal({
|
|
89
90
|
visible,
|
|
@@ -106,6 +106,8 @@
|
|
|
106
106
|
scrollEventThrottle={props.scrollEventThrottle}
|
|
107
107
|
keyboardShouldPersistTaps={props.keyboardShouldPersistTaps}
|
|
108
108
|
keyboardDismissMode={props.keyboardDismissMode}
|
|
109
|
+
removeClippedSubviews={props.removeClippedSubviews}
|
|
110
|
+
nestedScrollEnabled={props.nestedScrollEnabled}
|
|
109
111
|
style={props.style}
|
|
110
112
|
contentContainerStyle={props.contentContainerStyle}
|
|
111
113
|
class={props.class}
|
|
@@ -109,6 +109,10 @@
|
|
|
109
109
|
|
|
110
110
|
// The one folded state cell — the Svelte twin of Vue's plain listState / React's stateRef.
|
|
111
111
|
const listState: IListState<ItemT> = createInitialListState<ItemT>();
|
|
112
|
+
// Plain Map by design, like listState above: separatorVersion (below) is the manual
|
|
113
|
+
// invalidation signal a read tracks — a SvelteMap would double-track and still miss nothing,
|
|
114
|
+
// but adds reactive overhead this hot path doesn't need.
|
|
115
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
112
116
|
const separatorOverrides = new Map<number, Partial<ISeparatorProps<ItemT>>>();
|
|
113
117
|
let viewableTimer: ReturnType<typeof setTimeout> | null = null;
|
|
114
118
|
let batchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -154,6 +158,8 @@
|
|
|
154
158
|
scrollEventThrottle: props.scrollEventThrottle,
|
|
155
159
|
keyboardShouldPersistTaps: props.keyboardShouldPersistTaps,
|
|
156
160
|
keyboardDismissMode: props.keyboardDismissMode,
|
|
161
|
+
removeClippedSubviews: props.removeClippedSubviews,
|
|
162
|
+
nestedScrollEnabled: props.nestedScrollEnabled,
|
|
157
163
|
style: props.style,
|
|
158
164
|
contentContainerStyle: props.contentContainerStyle,
|
|
159
165
|
class: props.class,
|
|
@@ -437,11 +443,12 @@
|
|
|
437
443
|
};
|
|
438
444
|
});
|
|
439
445
|
|
|
446
|
+
// VirtualizedList.js: `[inversionStyle, style]` — the app's style can override the flip.
|
|
440
447
|
const resolvedStyle = $derived(
|
|
441
448
|
narrowed.inverted
|
|
442
449
|
? [
|
|
443
|
-
narrowed.style,
|
|
444
450
|
narrowed.horizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE,
|
|
451
|
+
narrowed.style,
|
|
445
452
|
]
|
|
446
453
|
: narrowed.style,
|
|
447
454
|
);
|
|
@@ -482,6 +489,13 @@
|
|
|
482
489
|
}
|
|
483
490
|
if (narrowed.keyboardDismissMode !== undefined)
|
|
484
491
|
bag.keyboardDismissMode = narrowed.keyboardDismissMode;
|
|
492
|
+
// RN's VirtualizedList spreads its props onto the ScrollView, this one included.
|
|
493
|
+
if (narrowed.removeClippedSubviews !== undefined)
|
|
494
|
+
bag.removeClippedSubviews = narrowed.removeClippedSubviews;
|
|
495
|
+
if (narrowed.nestedScrollEnabled !== undefined)
|
|
496
|
+
bag.nestedScrollEnabled = narrowed.nestedScrollEnabled;
|
|
497
|
+
// VirtualizedList.js:1111 — Android moves the scrollbar back after the `scale: -1` flip.
|
|
498
|
+
if (narrowed.inverted === true) bag.isInvertedVirtualizedList = true;
|
|
485
499
|
// `stickyHeaderIndices` is deliberately NOT forwarded. The behavior honours it by numbering the
|
|
486
500
|
// owner's own PAINT children, and this list's indices are into the DATA stream — a windowed
|
|
487
501
|
// list paints a spacer, a header and a slice, so index 3 of the data is almost never paint
|
|
@@ -62,6 +62,8 @@ export interface IVirtualizedListProps<ItemT> extends IAccessibilityProps, IAria
|
|
|
62
62
|
scrollEventThrottle?: number;
|
|
63
63
|
keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
|
|
64
64
|
keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
|
|
65
|
+
removeClippedSubviews?: boolean;
|
|
66
|
+
nestedScrollEnabled?: boolean;
|
|
65
67
|
style?: IStyleProp<IViewStyle>;
|
|
66
68
|
contentContainerStyle?: IStyleProp<IViewStyle>;
|
|
67
69
|
class?: ISvelteClassValue;
|
|
@@ -247,6 +247,8 @@
|
|
|
247
247
|
scrollEventThrottle={props.scrollEventThrottle}
|
|
248
248
|
keyboardShouldPersistTaps={props.keyboardShouldPersistTaps}
|
|
249
249
|
keyboardDismissMode={props.keyboardDismissMode}
|
|
250
|
+
removeClippedSubviews={props.removeClippedSubviews}
|
|
251
|
+
nestedScrollEnabled={props.nestedScrollEnabled}
|
|
250
252
|
style={props.style}
|
|
251
253
|
contentContainerStyle={props.contentContainerStyle}
|
|
252
254
|
class={props.class}
|
|
@@ -61,6 +61,8 @@ export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps
|
|
|
61
61
|
scrollEventThrottle?: number;
|
|
62
62
|
keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
|
|
63
63
|
keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
|
|
64
|
+
removeClippedSubviews?: boolean;
|
|
65
|
+
nestedScrollEnabled?: boolean;
|
|
64
66
|
style?: IStyleProp<IViewStyle>;
|
|
65
67
|
contentContainerStyle?: IStyleProp<IViewStyle>;
|
|
66
68
|
class?: ISvelteClassValue;
|
|
@@ -321,3 +321,87 @@ export const CANONICAL_PROP_NAMES = [
|
|
|
321
321
|
// collide: the derivation asserts it (`canonical-prop-names.test.ts`), because two props whose
|
|
322
322
|
// names differ only in case would make the repair a coin toss.
|
|
323
323
|
export const CANONICAL_BY_LOWER = new Map(CANONICAL_PROP_NAMES.map(name => [name.toLowerCase(), name]));
|
|
324
|
+
// The subset whose type takes a boolean and no string anywhere: for these "" can only be the HTML
|
|
325
|
+
// shorthand for true. Svelte puts a static attribute on a hyphen-less tag (`<view accessible>`)
|
|
326
|
+
// into the template as `accessible=""`, and Android's ViewManager throws on a String where it
|
|
327
|
+
// casts to Boolean. Same derivation, same guard (`canonical-prop-names.test.ts`).
|
|
328
|
+
export const BOOLEAN_PROP_NAMES = new Set([
|
|
329
|
+
'accessibilityElementsHidden',
|
|
330
|
+
'accessibilityIgnoresInvertColors',
|
|
331
|
+
'accessibilityRespondsToUserInteraction',
|
|
332
|
+
'accessibilityShowsLargeContentViewer',
|
|
333
|
+
'accessibilityViewIsModal',
|
|
334
|
+
'accessible',
|
|
335
|
+
'adjustsFontSizeToFit',
|
|
336
|
+
'allowFontScaling',
|
|
337
|
+
'allowSwipeDismissal',
|
|
338
|
+
'alwaysBounceHorizontal',
|
|
339
|
+
'alwaysBounceVertical',
|
|
340
|
+
'android_disableSound',
|
|
341
|
+
'animated',
|
|
342
|
+
'animating',
|
|
343
|
+
'aria-busy',
|
|
344
|
+
'aria-checked',
|
|
345
|
+
'aria-disabled',
|
|
346
|
+
'aria-expanded',
|
|
347
|
+
'aria-hidden',
|
|
348
|
+
'aria-modal',
|
|
349
|
+
'aria-selected',
|
|
350
|
+
'autoCorrect',
|
|
351
|
+
'autoFocus',
|
|
352
|
+
'automaticallyAdjustKeyboardInsets',
|
|
353
|
+
'blockNativeResponder',
|
|
354
|
+
'blurOnSubmit',
|
|
355
|
+
'bounces',
|
|
356
|
+
'bouncesZoom',
|
|
357
|
+
'cancelable',
|
|
358
|
+
'centerContent',
|
|
359
|
+
'collapsable',
|
|
360
|
+
'directionalLockEnabled',
|
|
361
|
+
'disableIntervalMomentum',
|
|
362
|
+
'disabled',
|
|
363
|
+
'editable',
|
|
364
|
+
'enabled',
|
|
365
|
+
'focusable',
|
|
366
|
+
'hardwareAccelerated',
|
|
367
|
+
'hasTVPreferredFocus',
|
|
368
|
+
'hidden',
|
|
369
|
+
'hidesWhenStopped',
|
|
370
|
+
'highlighted',
|
|
371
|
+
'horizontal',
|
|
372
|
+
'invertStickyHeaders',
|
|
373
|
+
'inverted',
|
|
374
|
+
'keyboardShouldPersistTaps',
|
|
375
|
+
'multiline',
|
|
376
|
+
'navigationBarTranslucent',
|
|
377
|
+
'needsOffscreenAlphaCompositing',
|
|
378
|
+
'nestedScrollEnabled',
|
|
379
|
+
'networkActivityIndicatorVisible',
|
|
380
|
+
'pagingEnabled',
|
|
381
|
+
'persistentScrollbar',
|
|
382
|
+
'pinchGestureEnabled',
|
|
383
|
+
'progressiveRenderingEnabled',
|
|
384
|
+
'readOnly',
|
|
385
|
+
'refreshing',
|
|
386
|
+
'removeClippedSubviews',
|
|
387
|
+
'renderToHardwareTextureAndroid',
|
|
388
|
+
'screenReaderFocusable',
|
|
389
|
+
'scrollEnabled',
|
|
390
|
+
'secureTextEntry',
|
|
391
|
+
'selectTextOnFocus',
|
|
392
|
+
'selectable',
|
|
393
|
+
'shouldRasterizeIOS',
|
|
394
|
+
'showSoftInputOnFocus',
|
|
395
|
+
'showsHorizontalScrollIndicator',
|
|
396
|
+
'showsVerticalScrollIndicator',
|
|
397
|
+
'snapToEnd',
|
|
398
|
+
'snapToStart',
|
|
399
|
+
'statusBarTranslucent',
|
|
400
|
+
'stickySectionHeadersEnabled',
|
|
401
|
+
'testOnly_pressed',
|
|
402
|
+
'touchSoundDisabled',
|
|
403
|
+
'translucent',
|
|
404
|
+
'transparent',
|
|
405
|
+
'useForeground',
|
|
406
|
+
'visible',
|
|
407
|
+
]);
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { createElement, dlog, routeProp, toPublicInstance, } from '@symbiote-native/engine';
|
|
26
26
|
import { descriptorFor } from '@symbiote-native/components';
|
|
27
27
|
import { normalizeSvelteClass } from '../class-value.js';
|
|
28
|
-
import { CANONICAL_BY_LOWER, CANONICAL_PROP_NAMES, } from './canonical-prop-names.js';
|
|
28
|
+
import { BOOLEAN_PROP_NAMES, CANONICAL_BY_LOWER, CANONICAL_PROP_NAMES, } from './canonical-prop-names.js';
|
|
29
29
|
import { ShimNode } from './shim-node.js';
|
|
30
30
|
import { discoverStyleCacheKey } from './style-cache.js';
|
|
31
31
|
// The bag a not-yet-live element is diffed against in onMadeLive. A module constant rather than a
|
|
@@ -144,7 +144,9 @@ export class ShimElement extends ShimElementBase {
|
|
|
144
144
|
// one we do not know) is handed on untouched. See `canonical-prop-names.ts`.
|
|
145
145
|
setAttribute(name, value) {
|
|
146
146
|
(this.attributes ??= new Map()).set(name, value);
|
|
147
|
-
|
|
147
|
+
const canonical = CANONICAL_BY_LOWER.get(name) ?? name;
|
|
148
|
+
// `<view accessible>` arrives as "" — the HTML shorthand for true on a boolean prop.
|
|
149
|
+
this.writeBagKey(canonical, value === '' && BOOLEAN_PROP_NAMES.has(canonical) ? true : value);
|
|
148
150
|
}
|
|
149
151
|
getAttribute(name) {
|
|
150
152
|
return this.attributes?.get(name) ?? null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/svelte",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
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": "3.
|
|
77
|
-
"@symbiote-native/css-parser": "0.5.
|
|
76
|
+
"@symbiote-native/components": "3.1.1",
|
|
77
|
+
"@symbiote-native/css-parser": "0.5.1"
|
|
78
78
|
},
|
|
79
79
|
"peerDependencies": {
|
|
80
80
|
"svelte": ">=5.56.0",
|
|
81
81
|
"react-native": ">=0.86",
|
|
82
|
-
"@symbiote-native/engine": "^1.
|
|
82
|
+
"@symbiote-native/engine": "^1.3.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": "1.
|
|
89
|
-
"@symbiote-native/react": "3.0.
|
|
90
|
-
"@symbiote-native/test-utils": "0.4.
|
|
91
|
-
"@symbiote-native/vue": "3.0.
|
|
88
|
+
"@symbiote-native/engine": "1.3.0",
|
|
89
|
+
"@symbiote-native/react": "3.0.3",
|
|
90
|
+
"@symbiote-native/test-utils": "0.4.3",
|
|
91
|
+
"@symbiote-native/vue": "3.0.3"
|
|
92
92
|
},
|
|
93
93
|
"scripts": {
|
|
94
94
|
"typecheck": "tsc --build",
|