@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 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. Every component is a `.svelte` SFC — there's no TSX
32
- alternative the way Vue offers — so the Metro wiring below isn't optional. `npx @symbiote-native/cli new`
33
- sets this up for a new app; wiring it into an existing one still follows
34
- [`examples/svelte`](../../examples/svelte) rather than a generator:
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()` and `scopedStyles()`
41
- preprocessors, so `svelte-check` and the editor catch the same things Metro's transformer
42
- already guards against at build time, and sets `compilerOptions: { fragments: 'tree', css:
43
- 'external' }`.
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
- The app is ordinary Svelte 5 — the native primitives are plain intrinsic tags, no import needed.
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 on-device on
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 setup](https://reactnative.dev/docs/set-up-your-environment) (Xcode,
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 POST-render $effect driving the visible->hidden transition (Svelte's $effect runs
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
- let state = $state(createInitialModalState(rawProps.visible === true));
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 === true);
42
- const shouldRender = $derived(shouldRenderModal(isVisible, state));
44
+ const isVisible = $derived(isModalVisible(resolved.visible));
45
+ const shouldRender = $derived(shouldRenderModal(isVisible, localState));
43
46
 
44
- // POST-render, mirroring state/modal.ts's contract: fires after the DOM update following the
45
- // render that used the OLD state, so a visible->hidden transition keeps the node mounted one
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
- state = modalReducer(
57
- state,
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
- ...passthrough
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;
@@ -1,2 +1,3 @@
1
1
  export declare const CANONICAL_PROP_NAMES: readonly string[];
2
2
  export declare const CANONICAL_BY_LOWER: ReadonlyMap<string, string>;
3
+ export declare const BOOLEAN_PROP_NAMES: ReadonlySet<string>;
@@ -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
- this.writeBagKey(CANONICAL_BY_LOWER.get(name) ?? name, value);
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.1",
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.0.1",
77
- "@symbiote-native/css-parser": "0.5.0"
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.2.0"
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.2.0",
89
- "@symbiote-native/react": "3.0.1",
90
- "@symbiote-native/test-utils": "0.4.1",
91
- "@symbiote-native/vue": "3.0.1"
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",