@symbiote-native/svelte 3.0.4 → 3.0.5

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.
@@ -0,0 +1,10 @@
1
+ import type { ShimElement } from './element';
2
+ export type IKeyframe = Readonly<Record<string, string>>;
3
+ export type IShimAnimation = {
4
+ onfinish: (() => void) | null;
5
+ effect: unknown;
6
+ readonly playState: 'idle' | 'running' | 'finished';
7
+ readonly currentTime: number;
8
+ cancel(): void;
9
+ };
10
+ export declare function animateShimElement(element: ShimElement, keyframes: readonly IKeyframe[], duration: number): IShimAnimation;
@@ -0,0 +1,80 @@
1
+ // `Element.animate()`, the subset `svelte/transition`/`svelte/animate` call: Svelte's own tick
2
+ // loop already bakes each frame's `css(t, u)` into the keyframe array, so this only replays them
3
+ // over real time (`.currentTime`/`.playState`/`.onfinish`/`.effect`/`.cancel()` contract).
4
+ import { isSymbioteNode, requestCommitFor, routeProp, } from '@symbiote-native/engine';
5
+ import { areShimAnimationsEnabled } from './animations-gate.js';
6
+ function isRecord(value) {
7
+ return typeof value === 'object' && value !== null;
8
+ }
9
+ // `opacity` parses to a number, matching a real CSSStyleDeclaration read back; every other key
10
+ // (transform, filter, ...) passes through as the authored string.
11
+ function applyKeyframe(node, style, frame) {
12
+ const next = isRecord(style) ? { ...style } : {};
13
+ for (const [key, value] of Object.entries(frame)) {
14
+ next[key] = key === 'opacity' ? Number(value) : value;
15
+ }
16
+ routeProp(node, 'style', next);
17
+ // Our own rAF loop, not a framework render - nothing else publishes this write per frame.
18
+ requestCommitFor(node);
19
+ }
20
+ class ShimAnimation {
21
+ node;
22
+ currentStyle;
23
+ keyframes;
24
+ duration;
25
+ onfinish = null;
26
+ effect = null;
27
+ playState = 'idle';
28
+ startTime = undefined;
29
+ rafId = undefined;
30
+ cancelled = false;
31
+ constructor(node, currentStyle, keyframes, duration) {
32
+ this.node = node;
33
+ this.currentStyle = currentStyle;
34
+ this.keyframes = keyframes;
35
+ this.duration = duration;
36
+ if (node === undefined || keyframes.length === 0 || duration <= 0) {
37
+ this.playState = 'finished';
38
+ // Guarded like `tick`: a same-tick `cancel()` (Svelte replacing this animation before the
39
+ // microtask runs) must not fire a handler for a transition that never got to play.
40
+ void Promise.resolve().then(() => {
41
+ if (!this.cancelled)
42
+ this.onfinish?.();
43
+ });
44
+ return;
45
+ }
46
+ this.playState = 'running';
47
+ this.rafId = requestAnimationFrame(this.tick);
48
+ }
49
+ get currentTime() {
50
+ return this.startTime === undefined ? 0 : Date.now() - this.startTime;
51
+ }
52
+ tick = (now) => {
53
+ if (this.cancelled || this.node === undefined)
54
+ return;
55
+ this.startTime ??= now;
56
+ const progress = Math.min((now - this.startTime) / this.duration, 1);
57
+ const index = Math.round(progress * (this.keyframes.length - 1));
58
+ const frame = this.keyframes[index];
59
+ if (frame !== undefined)
60
+ applyKeyframe(this.node, this.currentStyle, frame);
61
+ if (progress >= 1) {
62
+ this.playState = 'finished';
63
+ this.onfinish?.();
64
+ return;
65
+ }
66
+ this.rafId = requestAnimationFrame(this.tick);
67
+ };
68
+ cancel() {
69
+ this.cancelled = true;
70
+ this.playState = 'idle';
71
+ if (this.rafId !== undefined)
72
+ cancelAnimationFrame(this.rafId);
73
+ }
74
+ }
75
+ // A node not yet live plays no animation - the same shape Svelte's own dummy zero-duration
76
+ // animation already takes, so a caller sees a normal, already-finished Animation.
77
+ export function animateShimElement(element, keyframes, duration) {
78
+ const node = element.engineNode;
79
+ return new ShimAnimation(areShimAnimationsEnabled() && isSymbioteNode(node) ? node : undefined, element.p.style, keyframes, duration);
80
+ }
@@ -0,0 +1,2 @@
1
+ export declare function setShimAnimationsEnabled(value: boolean): void;
2
+ export declare function areShimAnimationsEnabled(): boolean;
@@ -0,0 +1,10 @@
1
+ // TODO: включить, когда API стабилизируется: `animate:flip` работает, shuffle-реордер нет
2
+ // Экран `TransitionAnimateDemo` снят с Canary, вернуть вместе с включением
3
+ // Пока выключено для потребителя, тесты включают через `setShimAnimationsEnabled`
4
+ let enabled = false;
5
+ export function setShimAnimationsEnabled(value) {
6
+ enabled = value;
7
+ }
8
+ export function areShimAnimationsEnabled() {
9
+ return enabled;
10
+ }
@@ -0,0 +1,3 @@
1
+ import type { ShimElement } from './element';
2
+ export type IComputedStyle = Readonly<Record<string, string>>;
3
+ export declare function computedStyleOf(element: ShimElement): IComputedStyle;
@@ -0,0 +1,34 @@
1
+ // The bare global `getComputedStyle`, which svelte/transition's fade/fly/scale/slide/blur call
2
+ // directly, not `document.getComputedStyle`. A Proxy, since a caller reads an arbitrary key built
3
+ // at runtime (slide derives `paddingTop`/`marginBottom` from its `axis` option).
4
+ function isRecord(value) {
5
+ return typeof value === 'object' && value !== null;
6
+ }
7
+ const DEFAULTS = {
8
+ opacity: '1',
9
+ transform: 'none',
10
+ filter: 'none',
11
+ display: 'flex',
12
+ strokeLinecap: 'butt',
13
+ strokeWidth: '0',
14
+ };
15
+ function styleValueOf(element, key) {
16
+ const style = element.p.style;
17
+ const raw = isRecord(style) ? style[key] : undefined;
18
+ if (raw !== undefined)
19
+ return typeof raw === 'number' ? `${raw}px` : String(raw);
20
+ // Two tokens, not `DEFAULTS`' single `'0px'` - `flip()` destructures this into [ox, oy] and
21
+ // divides by clientWidth/Height, so a lone token leaves oy as NaN (real default is `50% 50%`).
22
+ if (key === 'transformOrigin') {
23
+ const { width, height } = element.getBoundingClientRect();
24
+ return `${width / 2}px ${height / 2}px`;
25
+ }
26
+ return DEFAULTS[key] ?? '0px';
27
+ }
28
+ export function computedStyleOf(element) {
29
+ return new Proxy({}, {
30
+ get(_target, key) {
31
+ return typeof key === 'string' ? styleValueOf(element, key) : undefined;
32
+ },
33
+ });
34
+ }
@@ -2,6 +2,7 @@ import { ShimElement } from './element';
2
2
  import { ShimText } from './text';
3
3
  import { ShimComment } from './comment';
4
4
  import { ShimDocumentFragment } from './document-fragment';
5
+ import { type IComputedStyle } from './computed-style';
5
6
  export declare class ShimDocument {
6
7
  readonly body: ShimElement;
7
8
  createElement(tag: string, options?: {
@@ -17,7 +18,7 @@ export declare class ShimDocument {
17
18
  importNode(node: ShimText, deep: boolean): ShimText;
18
19
  importNode(node: ShimComment, deep: boolean): ShimComment;
19
20
  importNode(node: ShimDocumentFragment, deep: boolean): ShimDocumentFragment;
20
- getComputedStyle(): Record<string, never>;
21
+ getComputedStyle(element: ShimElement): IComputedStyle;
21
22
  }
22
23
  export declare function getShimDocument(): ShimDocument;
23
24
  export declare function resetShimDocumentForTests(): void;
@@ -1,14 +1,12 @@
1
- // The shim's `document` singleton — the six factories `init_operations()` and `from_tree`
2
- // require (svelte-adapter-dom-shim skill §3b), plus the two cheap stubs §4 calls for
3
- // explicitly (`document.body`, `getComputedStyle`) so a forbidden/dead path degrades to a
4
- // harmless no-op instead of a hard crash. No `<svelte:head|window|body|document>` support
5
- // beyond that stub — those tags are rejected at build time by the preprocessor (§7), not
6
- // handled here.
1
+ // The shim's `document` singleton - the six factories `init_operations()`/`from_tree` require,
2
+ // plus `document.body` (a minimal unattached node) and `getComputedStyle` (computed-style.ts).
3
+ // No `<svelte:head|window|body|document>` - rejected at build time by the preprocessor.
7
4
  import { ShimElement } from './element.js';
8
5
  import { ShimText } from './text.js';
9
6
  import { ShimComment } from './comment.js';
10
7
  import { ShimDocumentFragment } from './document-fragment.js';
11
8
  import { registerShimDocumentFactory } from './shim-node.js';
9
+ import { computedStyleOf } from './computed-style.js';
12
10
  export class ShimDocument {
13
11
  // The delegation root real Svelte events bubble toward (`dom/elements/events.js:122`).
14
12
  // Our own event names never delegate (§5c: no camelCase name matches Svelte's 23-name
@@ -39,11 +37,10 @@ export class ShimDocument {
39
37
  importNode(node, deep) {
40
38
  return node.cloneNode(deep);
41
39
  }
42
- // §4: not read by any mandatory path; stubbed so a forbidden transition (which we reject
43
- // at build time anyway, §7) degrades to an inert value rather than a hard crash if it is
44
- // ever reached some other way.
45
- getComputedStyle() {
46
- return {};
40
+ // Kept in step with the bare global patch-globals.ts installs (computed-style.ts is the one
41
+ // implementation both call).
42
+ getComputedStyle(element) {
43
+ return computedStyleOf(element);
47
44
  }
48
45
  }
49
46
  let singleton;
@@ -1,4 +1,5 @@
1
- import { type ISymbioteNode } from '@symbiote-native/engine';
1
+ import { type IDomRect, type ISymbioteNode } from '@symbiote-native/engine';
2
+ import { type IKeyframe, type IShimAnimation } from './animation';
2
3
  import { ShimNode } from './shim-node';
3
4
  export type IShimPropBag = Record<string, unknown>;
4
5
  export declare abstract class ShimElementBase extends ShimNode {
@@ -30,6 +31,19 @@ export declare class ShimElement extends ShimElementBase {
30
31
  addEventListener(name: string, handler: (event: unknown) => void): void;
31
32
  removeEventListener(name: string): void;
32
33
  cloneNode(deep?: boolean): ShimElement;
34
+ get parentElement(): ShimElement | null;
35
+ get clientWidth(): number;
36
+ get clientHeight(): number;
37
+ getBoundingClientRect(): IDomRect & {
38
+ top: number;
39
+ left: number;
40
+ right: number;
41
+ bottom: number;
42
+ };
43
+ dispatchEvent(): boolean;
44
+ animate(keyframes: readonly IKeyframe[], options: {
45
+ duration: number;
46
+ }): IShimAnimation;
33
47
  createEngineNode(): ISymbioteNode;
34
48
  onMadeLive(): void;
35
49
  }
@@ -22,9 +22,11 @@
22
22
  // A per-key diff in the setter is MANDATORY (§3g(c)), not an optimization: Svelte's
23
23
  // `set_custom_element_data` has no early-out guard and runs on every effect re-fire, so
24
24
  // without the diff every prop gets rewritten whenever any one of them changes.
25
- import { createElement, dlog, routeProp, toPublicInstance, } from '@symbiote-native/engine';
25
+ import { createElement, dlog, getBoundingClientRect, isSymbioteNode, routeProp, toPublicInstance, } from '@symbiote-native/engine';
26
26
  import { descriptorFor } from '@symbiote-native/components';
27
27
  import { normalizeSvelteClass } from '../class-value.js';
28
+ import { animateShimElement, } from './animation.js';
29
+ import { areShimAnimationsEnabled } from './animations-gate.js';
28
30
  import { BOOLEAN_PROP_NAMES, CANONICAL_BY_LOWER, CANONICAL_PROP_NAMES, } from './canonical-prop-names.js';
29
31
  import { ShimNode } from './shim-node.js';
30
32
  import { discoverStyleCacheKey } from './style-cache.js';
@@ -251,6 +253,48 @@ export class ShimElement extends ShimElementBase {
251
253
  }
252
254
  return clone;
253
255
  }
256
+ // `flip`'s `get_zoom()` walks this until `null`; without it the walk hit `undefined` and threw
257
+ // `Cannot read properties of undefined ('parentElement')`, aborting the whole reorder.
258
+ get parentElement() {
259
+ const result = this.parent instanceof ShimElement ? this.parent : null;
260
+ dlog(`parentElement tag=${this.tagName} -> ${result === null ? 'null' : result.tagName}`);
261
+ return result;
262
+ }
263
+ // `flip` divides by these for its scale factor; unset they read as `NaN`, not zero.
264
+ get clientWidth() {
265
+ return this.getBoundingClientRect().width;
266
+ }
267
+ get clientHeight() {
268
+ return this.getBoundingClientRect().height;
269
+ }
270
+ // Real DOM shape (top/left/right/bottom derived), for svelte/animate's `flip` and
271
+ // svelte/transition's `crossfade`, both of which measure before and after a reorder.
272
+ getBoundingClientRect() {
273
+ const node = this.engineNode;
274
+ const rect = areShimAnimationsEnabled() && isSymbioteNode(node)
275
+ ? getBoundingClientRect(node, false)
276
+ : undefined;
277
+ dlog(`getBoundingClientRect tag=${this.tagName} hasNode=${isSymbioteNode(node)} rect=${JSON.stringify(rect)}`);
278
+ const { x, y, width, height } = rect ?? { x: 0, y: 0, width: 0, height: 0 };
279
+ return {
280
+ x,
281
+ y,
282
+ width,
283
+ height,
284
+ top: y,
285
+ left: x,
286
+ right: x + width,
287
+ bottom: y + height,
288
+ };
289
+ }
290
+ // `transitions.js`'s `dispatch_event` fires introstart/introend/outrostart/outroend at this;
291
+ // nothing in this adapter listens, so a no-op is the correct answer, not a missing method.
292
+ dispatchEvent() {
293
+ return true;
294
+ }
295
+ animate(keyframes, options) {
296
+ return animateShimElement(this, keyframes, options.duration);
297
+ }
254
298
  // measure/measureInWindow/measureLayout/setNativeProps/focus/blur — the imperative API a
255
299
  // `bind:this` host ref hands back — ride on the engine node's prototype, so toPublicInstance is
256
300
  // the identity and this reads the same as Vue's renderer (renderer/index.ts) and React's
@@ -29,6 +29,7 @@ import { ShimComment } from './comment.js';
29
29
  import { ShimDocumentFragment } from './document-fragment.js';
30
30
  import { ShimNode } from './shim-node.js';
31
31
  import { getShimDocument } from './document.js';
32
+ import { computedStyleOf } from './computed-style.js';
32
33
  const PATCHED_KEYS = [
33
34
  'Node',
34
35
  'Element',
@@ -40,7 +41,36 @@ const PATCHED_KEYS = [
40
41
  'document',
41
42
  'customElements',
42
43
  'HTMLMediaElement',
44
+ 'getComputedStyle',
45
+ 'CustomEvent',
43
46
  ];
47
+ // `svelte/transition`'s fade/fly/scale/slide/blur call this BARE global, not
48
+ // `document.getComputedStyle`, so it must be a real assigned property too.
49
+ function getComputedStyleGlobal(element) {
50
+ return element instanceof ShimElement
51
+ ? computedStyleOf(element)
52
+ : computedStyleOf(new ShimElement('view'));
53
+ }
54
+ // `createEventDispatcher`'s own event object; `svelte/events`'s `on()` also constructs one.
55
+ // RN installs no CustomEvent of its own, so this must be a real assigned property, matching the
56
+ // same class of trap as `customElements`/`HTMLMediaElement` below.
57
+ class FakeCustomEvent {
58
+ type;
59
+ detail;
60
+ bubbles;
61
+ cancelable;
62
+ defaultPrevented = false;
63
+ constructor(type, options) {
64
+ this.type = type;
65
+ this.detail = options?.detail;
66
+ this.bubbles = options?.bubbles ?? false;
67
+ this.cancelable = options?.cancelable ?? false;
68
+ }
69
+ preventDefault() {
70
+ if (this.cancelable)
71
+ this.defaultPrevented = true;
72
+ }
73
+ }
44
74
  // Real DOM's `CustomElementRegistry`. `set_custom_element_data` (svelte's
45
75
  // dom/elements/attributes.js) reads the BARE global `customElements` unconditionally, with no
46
76
  // `typeof` guard — an undeclared global throws ReferenceError, not just `undefined`, so this
@@ -81,6 +111,8 @@ export function patchGlobals() {
81
111
  document: g.document,
82
112
  customElements: g.customElements,
83
113
  HTMLMediaElement: g.HTMLMediaElement,
114
+ getComputedStyle: g.getComputedStyle,
115
+ CustomEvent: g.CustomEvent,
84
116
  };
85
117
  g.Node = ShimNode;
86
118
  // ShimElementBase, NOT ShimElement: `get_setters` stops AT `Element.prototype`, so pointing this
@@ -97,6 +129,8 @@ export function patchGlobals() {
97
129
  g.document = getShimDocument();
98
130
  g.customElements = FAKE_CUSTOM_ELEMENT_REGISTRY;
99
131
  g.HTMLMediaElement = FakeHTMLMediaElement;
132
+ g.getComputedStyle = getComputedStyleGlobal;
133
+ g.CustomEvent = FakeCustomEvent;
100
134
  dlog('svelte dom-shim: patchGlobals installed');
101
135
  }
102
136
  export function restoreGlobals() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/svelte",
3
- "version": "3.0.4",
3
+ "version": "3.0.5",
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.1.2",
76
+ "@symbiote-native/components": "3.1.3",
77
77
  "@symbiote-native/css-parser": "0.5.2"
78
78
  },
79
79
  "peerDependencies": {
80
80
  "svelte": ">=5.56.0",
81
81
  "react-native": ">=0.86",
82
- "@symbiote-native/engine": "^1.3.1"
82
+ "@symbiote-native/engine": "^1.4.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.3.1",
89
- "@symbiote-native/react": "3.0.4",
90
- "@symbiote-native/test-utils": "0.4.4",
91
- "@symbiote-native/vue": "3.0.4"
88
+ "@symbiote-native/engine": "1.4.0",
89
+ "@symbiote-native/test-utils": "0.4.5",
90
+ "@symbiote-native/react": "3.1.0",
91
+ "@symbiote-native/vue": "3.1.0"
92
92
  },
93
93
  "scripts": {
94
94
  "typecheck": "tsc --build",