@octane-xplat/lottie 0.0.1 → 0.9.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.
Files changed (58) hide show
  1. package/README.md +36 -2
  2. package/dist/native/cx.mobile.js +11 -0
  3. package/dist/native/escape-props.js +39 -0
  4. package/dist/native/index.js +182 -0
  5. package/dist/native/layout-child.js +38 -0
  6. package/dist/native/octane-client-build.json +8 -0
  7. package/dist/native/vendor/ui-lottie/index.android.js +286 -0
  8. package/dist/native/vendor/ui-lottie/index.ios.js +214 -0
  9. package/dist/native/vendor/ui-lottie/src/lottie/index.common.js +50 -0
  10. package/dist/native/vendor/ui-lottie/src/lottie/utils.js +6 -0
  11. package/dist/web/escape-props.web.js +17 -0
  12. package/dist/web/index.js +332 -0
  13. package/dist/web/layout-child.web.js +47 -0
  14. package/dist/web/octane-client-build.json +8 -0
  15. package/package.json +81 -10
  16. package/platforms/android/include.gradle +7 -0
  17. package/platforms/android/java/com/nativescript/lottie/LottieAnimationView.java +92 -0
  18. package/platforms/android/lottie.aar +0 -0
  19. package/platforms/android/native-api-usage.json +16 -0
  20. package/platforms/ios/Podfile +5 -0
  21. package/src/Lottie.tsrx +161 -0
  22. package/src/Lottie.web.tsrx +135 -0
  23. package/src/cx.mobile.ts +8 -0
  24. package/src/escape-props.ts +65 -0
  25. package/src/escape-props.web.ts +25 -0
  26. package/src/index.ts +2 -0
  27. package/src/index.web.ts +2 -0
  28. package/src/layout-child.ts +57 -0
  29. package/src/layout-child.web.ts +108 -0
  30. package/src/native-elements.d.ts +8 -0
  31. package/src/props.ts +128 -0
  32. package/src/references.d.ts +3 -0
  33. package/src/vendor/ui-lottie/LICENSE.md +21 -0
  34. package/src/vendor/ui-lottie/src/lottie/index.android.ts +447 -0
  35. package/src/vendor/ui-lottie/src/lottie/index.common.ts +84 -0
  36. package/src/vendor/ui-lottie/src/lottie/index.d.ts +133 -0
  37. package/src/vendor/ui-lottie/src/lottie/index.ios.ts +345 -0
  38. package/src/vendor/ui-lottie/src/lottie/typings/android.d.ts +4149 -0
  39. package/src/vendor/ui-lottie/src/lottie/typings/android.nativescript.d.ts +12 -0
  40. package/src/vendor/ui-lottie/src/lottie/typings/dotlottie.ios.d.ts +14 -0
  41. package/src/vendor/ui-lottie/src/lottie/typings/ios.d.ts +384 -0
  42. package/src/vendor/ui-lottie/src/lottie/utils.ts +9 -0
  43. package/types/generated/.tsrx-typegen-manifest.json +18 -0
  44. package/types/generated/Lottie.d.ts +7 -0
  45. package/types/generated/Lottie.web.d.ts +7 -0
  46. package/types/generated/cx.mobile.d.ts +6 -0
  47. package/types/generated/escape-props.d.ts +15 -0
  48. package/types/generated/escape-props.web.d.ts +5 -0
  49. package/types/generated/index.d.ts +2 -0
  50. package/types/generated/index.web.d.ts +2 -0
  51. package/types/generated/layout-child.d.ts +6 -0
  52. package/types/generated/layout-child.web.d.ts +15 -0
  53. package/types/generated/props.d.ts +106 -0
  54. package/types/generated/vendor/ui-lottie/src/lottie/index.common.d.ts +30 -0
  55. package/types/generated/vendor/ui-lottie/src/lottie/index.ios.d.ts +35 -0
  56. package/types/generated/vendor/ui-lottie/src/lottie/utils.d.ts +1 -0
  57. package/index.d.ts +0 -1
  58. package/index.js +0 -2
package/package.json CHANGED
@@ -1,18 +1,89 @@
1
1
  {
2
2
  "name": "@octane-xplat/lottie",
3
- "version": "0.0.1",
3
+ "version": "0.9.0",
4
4
  "description": "Lottie animation leaf for Octane xplat — lottie-web on web + vendored @nativescript-community/ui-lottie (fork fixes) on native",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "https://github.com/octane-xplat/octane-xplat.git",
7
+ "url": "https://github.com/octane-xplat/octane-xplat",
8
8
  "directory": "packages/lottie"
9
9
  },
10
- "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/lottie",
11
- "main": "./index.js",
12
- "types": "./index.d.ts",
13
10
  "files": [
14
- "README.md",
15
- "index.js",
16
- "index.d.ts"
17
- ]
18
- }
11
+ "dist",
12
+ "types",
13
+ "platforms",
14
+ "src/index.ts",
15
+ "src/index.web.ts",
16
+ "src/Lottie.tsrx",
17
+ "src/Lottie.web.tsrx",
18
+ "src/props.ts",
19
+ "src/native-elements.d.ts",
20
+ "src/references.d.ts",
21
+ "src/cx.mobile.ts",
22
+ "src/escape-props.ts",
23
+ "src/escape-props.web.ts",
24
+ "src/layout-child.ts",
25
+ "src/layout-child.web.ts",
26
+ "src/vendor/ui-lottie/LICENSE.md",
27
+ "src/vendor/ui-lottie/src/lottie/index.android.ts",
28
+ "src/vendor/ui-lottie/src/lottie/index.common.ts",
29
+ "src/vendor/ui-lottie/src/lottie/index.d.ts",
30
+ "src/vendor/ui-lottie/src/lottie/index.ios.ts",
31
+ "src/vendor/ui-lottie/src/lottie/utils.ts",
32
+ "src/vendor/ui-lottie/src/lottie/typings"
33
+ ],
34
+ "type": "module",
35
+ "exports": {
36
+ ".": {
37
+ "web": {
38
+ "types": "./types/generated/index.web.d.ts",
39
+ "default": "./dist/web/index.js"
40
+ },
41
+ "native": {
42
+ "types": "./types/generated/index.d.ts",
43
+ "default": "./dist/native/index.js"
44
+ },
45
+ "types": "./types/generated/index.d.ts",
46
+ "default": "./dist/native/index.js"
47
+ }
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "dependencies": {
53
+ "lottie-web": "5.13.0"
54
+ },
55
+ "devDependencies": {
56
+ "@nativescript-community/octane": "0.2.1",
57
+ "@nativescript/core": "9.1.2",
58
+ "@nativescript/types": "9.0.0",
59
+ "@octanejs/vite-plugin": "0.1.61",
60
+ "@tsrx/typescript-plugin": "0.4.11",
61
+ "octane": "0.6.3",
62
+ "tsrx-typegen": "0.2.1",
63
+ "typescript": "5.9.3",
64
+ "vite": "8.3.0"
65
+ },
66
+ "peerDependencies": {
67
+ "@nativescript-community/octane": ">=0.2.1 <1",
68
+ "@nativescript/core": ">=9.1.0 <10",
69
+ "octane": ">=0.6.3 <1"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "@nativescript-community/octane": {
73
+ "optional": true
74
+ },
75
+ "@nativescript/core": {
76
+ "optional": true
77
+ }
78
+ },
79
+ "nativescript": {
80
+ "platforms": {
81
+ "android": "6.0.0",
82
+ "ios": "6.0.1"
83
+ }
84
+ },
85
+ "scripts": {
86
+ "build": "vite build && vite build --mode native && tsrx-typegen --target octane && node ../../scripts/check-native-dist.mjs",
87
+ "test:packed": "node tests/packed-consumer.mjs"
88
+ }
89
+ }
@@ -0,0 +1,7 @@
1
+
2
+ dependencies {
3
+ def androidXAppCompatVersion = project.hasProperty("androidXAppCompat") ? project.androidXAppCompat : "1.3.1"
4
+ implementation "androidx.appcompat:appcompat:$androidXAppCompatVersion"
5
+ def lottieVersion = project.hasProperty("lottieVersion") ? project.lottieVersion : "5.2.0"
6
+ implementation "com.airbnb.android:lottie:$lottieVersion"
7
+ }
@@ -0,0 +1,92 @@
1
+ package com.nativescript.lottie;
2
+
3
+ import androidx.core.view.ViewCompat;
4
+ import android.view.View.MeasureSpec;
5
+ import android.graphics.drawable.Drawable;
6
+
7
+ public class LottieAnimationView extends com.airbnb.lottie.LottieAnimationView {
8
+ private double scaleW = 1;
9
+ private double scaleH = 1;
10
+
11
+ public LottieAnimationView(android.content.Context context) {
12
+ super(context);
13
+ }
14
+
15
+ @Override
16
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
17
+
18
+ int width = MeasureSpec.getSize(widthMeasureSpec);
19
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
20
+
21
+ int height = MeasureSpec.getSize(heightMeasureSpec);
22
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
23
+
24
+ Drawable drawable = this.getDrawable();
25
+ int measureWidth;
26
+ int measureHeight;
27
+ if (drawable != null) {
28
+ measureWidth = drawable.getIntrinsicWidth();
29
+ measureHeight = drawable.getIntrinsicHeight();
30
+ } else {
31
+ measureWidth = 0;
32
+ measureHeight = 0;
33
+ }
34
+
35
+ boolean finiteWidth = widthMode != MeasureSpec.UNSPECIFIED;
36
+ boolean finiteHeight = heightMode != MeasureSpec.UNSPECIFIED;
37
+
38
+ if (measureWidth != 0 && measureHeight != 0 && (finiteWidth || finiteHeight)) {
39
+ this.computeScaleFactor(width, height, finiteWidth, finiteHeight, measureWidth, measureHeight);
40
+ int resultW = (int) Math.round(measureWidth * this.scaleW);
41
+ int resultH = (int) Math.round(measureHeight * this.scaleH);
42
+
43
+ measureWidth = finiteWidth ? Math.min(resultW, width) : resultW;
44
+ measureHeight = finiteHeight ? Math.min(resultH, height) : resultH;
45
+ }
46
+
47
+ measureWidth += this.getPaddingLeft() + this.getPaddingRight();
48
+ measureHeight += this.getPaddingTop() + this.getPaddingBottom();
49
+
50
+ measureWidth = Math.max(measureWidth, getSuggestedMinimumWidth());
51
+ measureHeight = Math.max(measureHeight, getSuggestedMinimumHeight());
52
+
53
+ int widthSizeAndState = ViewCompat.resolveSizeAndState(measureWidth, widthMeasureSpec, 0);
54
+ int heightSizeAndState = ViewCompat.resolveSizeAndState(measureHeight, heightMeasureSpec, 0);
55
+
56
+ this.setMeasuredDimension(widthSizeAndState, heightSizeAndState);
57
+ }
58
+
59
+ private void computeScaleFactor(int measureWidth, int measureHeight, boolean widthIsFinite, boolean heightIsFinite, double nativeWidth, double nativeHeight) {
60
+
61
+ this.scaleW = 1;
62
+ this.scaleH = 1;
63
+
64
+ ScaleType scale = this.getScaleType();
65
+ if ((scale == ScaleType.CENTER_CROP || scale == ScaleType.FIT_CENTER || scale == ScaleType.FIT_XY) &&
66
+ (widthIsFinite || heightIsFinite)) {
67
+
68
+ this.scaleW = (nativeWidth > 0) ? measureWidth / nativeWidth : 0d;
69
+ this.scaleH = (nativeHeight > 0) ? measureHeight / nativeHeight : 0d;
70
+
71
+ if (!widthIsFinite) {
72
+ this.scaleW = scaleH;
73
+ } else if (!heightIsFinite) {
74
+ this.scaleH = scaleW;
75
+ } else {
76
+ // No infinite dimensions.
77
+ switch (scale) {
78
+ case FIT_CENTER:
79
+ this.scaleH = this.scaleW < this.scaleH ? this.scaleW : this.scaleH;
80
+ this.scaleW = this.scaleH;
81
+ break;
82
+ case CENTER_CROP:
83
+ this.scaleH = this.scaleW > this.scaleH ? this.scaleW : this.scaleH;
84
+ this.scaleW = this.scaleH;
85
+ break;
86
+ default:
87
+ break;
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
Binary file
@@ -0,0 +1,16 @@
1
+ {
2
+ "uses": [
3
+ "com.airbnb.lottie:LottieAnimationView",
4
+ "com.airbnb.lottie.model:KeyPath",
5
+ "com.airbnb.lottie.value:LottieValueCallback",
6
+ "com.airbnb.lottie:LottieProperty",
7
+ "com.airbnb.lottie:SimpleColorFilter",
8
+ "com.airbnb.lottie:RenderMode",
9
+ "android.animation:Animator",
10
+ "android.animation:Animator.AnimatorListener",
11
+ "com.airbnb.lottie:LottieResult",
12
+ "com.airbnb.lottie:LottieOnCompositionLoadedListener",
13
+ "com.airbnb.lottie:LottieComposition",
14
+ "com.airbnb.lottie:LottieCompositionFactory"
15
+ ]
16
+ }
@@ -0,0 +1,5 @@
1
+ # Pinned to the 4.4.1 branch HEAD — a branch reference resolves a moving
2
+ # target on every fresh pod install. The fork carries the
3
+ # CompatibleAnimation* ObjC shims + CompatibleLottieLogger this plugin needs.
4
+ pod 'lottie-ios', :git => 'https://github.com/farfromrefug/lottie-ios.git', :commit => 'fd75f8aaa47d9479fc43cf91dd1da557952f6c0c'
5
+ # pod 'lottie-ios', :path => '/Volumes/dev/ios/lottie-ios'
@@ -0,0 +1,161 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import { useCallback, useEffect, useRef } from 'octane';
3
+ import { registerElement } from '@nativescript-community/octane';
4
+ import { isAndroid } from '@nativescript/core';
5
+ import { LottieView } from './vendor/ui-lottie/src/lottie';
6
+ import type { LottieHandle, LottieProps } from './props';
7
+ import { cx } from './cx.mobile';
8
+ import { applyEscapeProps, nativeAccessibilityRole, nativeAccessibilityState } from './escape-props';
9
+ import { layoutChildProps } from './layout-child';
10
+
11
+ registerElement('xplatlottie', LottieView);
12
+
13
+ const clamp01 = (n: number) => Math.min(1, Math.max(0, n));
14
+
15
+ const STRETCH: Record<string, string> = {
16
+ contain: 'aspectFit',
17
+ cover: 'aspectFill',
18
+ fill: 'fill',
19
+ };
20
+
21
+ /** Duration in ms on both platforms — the plugin's `duration` reports
22
+ * seconds on iOS and ms on Android. */
23
+ function durationMs(v: LottieView): number {
24
+ const d = v.duration ?? 0;
25
+ return isAndroid ? d : Math.max(0, d) * 1000;
26
+ }
27
+
28
+ function currentProgress(v: LottieView): number {
29
+ // the native type is platform-divergent (LottieAnimationView vs
30
+ // CompatibleAnimationView) — runtime branch is the narrowing.
31
+ const nv = v.nativeViewProtected as any;
32
+ return isAndroid ? (nv?.getProgress?.() ?? 0) : (nv?.currentProgress ?? 0);
33
+ }
34
+
35
+ /** Stop = pause + reset playhead. iOS `stop()` resets to the start;
36
+ * Android's `cancelAnimation()` ends the animator and the next
37
+ * `playAnimation()` restarts from 0. */
38
+ function stopView(v: LottieView) {
39
+ if (isAndroid) v.cancelAnimation();
40
+ else v.nativeViewProtected?.stop();
41
+
42
+ v.progress = 0;
43
+ }
44
+
45
+ /** Lottie — vendored ui-lottie surface (see src/plugin). The plugin
46
+ * doesn't diff props, so src/play-state/speed/progress land imperatively
47
+ * in effects. `async` stays forced on: it keeps the load path uniform
48
+ * (one code path for inline/file/URL/.lottie) and avoids main-thread
49
+ * parse jank; autoPlay is re-applied when the composition lands. */
50
+ export function Lottie(props: LottieProps) @{
51
+ const host = useRef<LottieView | null>(null);
52
+ const appliedSrc = useRef<string | undefined>(undefined);
53
+ const loadedRef = useRef(false);
54
+
55
+ const bindHost = useCallback((v: LottieView) => {
56
+ host.current = v;
57
+ }, []);
58
+
59
+ const wantPlay = () => props.playing ?? props.autoPlay ?? true;
60
+
61
+ const onReady = () => {
62
+ const v = host.current;
63
+ if (!v || loadedRef.current) return;
64
+ loadedRef.current = true;
65
+ if (props.speed !== undefined) v.speed = props.speed;
66
+ if (props.progress !== undefined) v.progress = clamp01(props.progress);
67
+ props.onLoaded?.({ duration: durationMs(v) });
68
+ if (wantPlay() && !v.isAnimating()) v.playAnimation();
69
+ };
70
+
71
+ // Mount wiring — events first so a synchronous src assignment can't
72
+ // race past them.
73
+ useEffect(() => {
74
+ const v = host.current;
75
+ if (!v) return;
76
+ applyEscapeProps(v, props);
77
+ v.stretch = STRETCH[props.fit ?? 'contain'] as any;
78
+ v.async = true;
79
+
80
+ v.on(LottieView.compositionLoadedEvent, () => onReady());
81
+ v.on(LottieView.loadFailedEvent, (e: any) => {
82
+ loadedRef.current = false;
83
+ props.onError?.({ message: e?.error ? String(e.error) : 'load failed' });
84
+ });
85
+
86
+ // The vendored fix reports a single `false` on cancel — only a
87
+ // `true` finish reaches onEnded.
88
+ v.completionBlock = (finished: boolean) => {
89
+ if (finished) props.onEnded?.();
90
+ };
91
+
92
+ props.bind?.(handle(v));
93
+ }, []);
94
+
95
+ // src — applied imperatively after the listeners exist.
96
+ useEffect(() => {
97
+ const v = host.current;
98
+ if (!v) return;
99
+ const src = props.data ? JSON.stringify(props.data) : props.src;
100
+ if (src === appliedSrc.current) return;
101
+ appliedSrc.current = src;
102
+ loadedRef.current = false;
103
+ v.src = src ?? '';
104
+ }, [props.src, props.data]);
105
+
106
+ // Controlled `playing` — play state is a method call, not a prop.
107
+ useEffect(() => {
108
+ const v = host.current;
109
+ if (!v || props.playing === undefined) return;
110
+ if (props.playing) {
111
+ if (loadedRef.current) v.resumeAnimation();
112
+ // not loaded yet — onReady picks it up via wantPlay()
113
+ } else {
114
+ v.pauseAnimation();
115
+ }
116
+ }, [props.playing]);
117
+
118
+ useEffect(() => {
119
+ const v = host.current;
120
+ if (!v || props.progress === undefined || !loadedRef.current) return;
121
+ v.progress = clamp01(props.progress);
122
+ }, [props.progress]);
123
+
124
+ useEffect(() => {
125
+ const v = host.current;
126
+ if (!v || props.speed === undefined || !loadedRef.current) return;
127
+ v.speed = props.speed;
128
+ }, [props.speed]);
129
+
130
+ const handle = (v: LottieView): LottieHandle => ({
131
+ play: () => v.resumeAnimation(),
132
+ pause: () => v.pauseAnimation(),
133
+ stop: () => stopView(v),
134
+ seekTo: (p: number) => {
135
+ v.progress = clamp01(p);
136
+ },
137
+ setSpeed: (n: number) => {
138
+ v.speed = n;
139
+ },
140
+ progress: () => currentProgress(v),
141
+ duration: () => durationMs(v),
142
+ isPlaying: () => v.isAnimating(),
143
+ native: v,
144
+ });
145
+
146
+ <xplatlottie
147
+ ref={bindHost}
148
+ id={props.id}
149
+ className={cx(props.className)}
150
+ style={props.style}
151
+ {...layoutChildProps(props)}
152
+ loop={props.loop ?? false}
153
+ accessible={props.accessible}
154
+ accessibilityLabel={props.accessibilityLabel}
155
+ accessibilityRole={nativeAccessibilityRole(props.accessibilityRole) as any}
156
+ accessibilityHint={props.accessibilityHint}
157
+ accessibilityValue={props.accessibilityValue}
158
+ accessibilityState={nativeAccessibilityState(props.accessibilityState) as any}
159
+ accessibilityLiveRegion={props.accessibilityLiveRegion as any}
160
+ />
161
+ }
@@ -0,0 +1,135 @@
1
+ import lottie, { type AnimationItem } from 'lottie-web';
2
+ import { useEffect, useRef } from 'octane';
3
+ import type { LottieHandle, LottieProps } from './props';
4
+ import { applyEscapeProps } from './escape-props.web';
5
+ import { layoutChildProps } from './layout-child.web';
6
+
7
+ const PRESERVE: Record<string, string> = {
8
+ contain: 'xMidYMid meet',
9
+ cover: 'xMidYMid slice',
10
+ fill: 'none',
11
+ };
12
+
13
+ const clamp01 = (n: number) => Math.min(1, Math.max(0, n));
14
+
15
+ /** Lottie — lottie-web's svg renderer in a plain container div. Everything
16
+ * is imperative: `src`/`data` rebuild the AnimationItem, the rest are
17
+ * method calls applied in effects. Progress is normalized 0..1
18
+ * (lottie-web speaks frames); durations arrive in seconds and are
19
+ * surfaced as ms. */
20
+ export function Lottie(props: LottieProps) @{
21
+ const el = useRef<HTMLDivElement | null>(null);
22
+ const anim = useRef<AnimationItem | null>(null);
23
+ const ready = useRef(false);
24
+
25
+ const wantPlay = () => props.playing ?? props.autoPlay ?? true;
26
+
27
+ const build = (host: HTMLDivElement) => {
28
+ anim.current?.destroy();
29
+ anim.current = null;
30
+ ready.current = false;
31
+
32
+ const animationData = props.data ?? (props.src?.startsWith('{') ? JSON.parse(props.src) : undefined);
33
+ const path = animationData ? undefined : props.src;
34
+ if (!animationData && !path) return;
35
+
36
+ const a = lottie.loadAnimation({
37
+ container: host,
38
+ renderer: 'svg',
39
+ loop: props.loop ?? false,
40
+ autoplay: false, // leaf owns play state — uniform with native
41
+ animationData,
42
+ path,
43
+ rendererSettings: { preserveAspectRatio: PRESERVE[props.fit ?? 'contain'] },
44
+ });
45
+
46
+ anim.current = a;
47
+
48
+ a.addEventListener('DOMLoaded', () => {
49
+ ready.current = true;
50
+ if (props.speed !== undefined) a.setSpeed(props.speed);
51
+ if (props.progress !== undefined) seekTo(props.progress);
52
+ props.onLoaded?.({ duration: a.getDuration() * 1000 });
53
+ if (wantPlay()) a.play();
54
+ });
55
+
56
+ a.addEventListener('complete', () => props.onEnded?.());
57
+ a.addEventListener('data_failed', () => props.onError?.({ message: 'animation data failed to load' }));
58
+ a.addEventListener('error', () => props.onError?.({ message: 'animation failed' }));
59
+ };
60
+
61
+ const seekTo = (p: number) => {
62
+ const a = anim.current;
63
+ if (!a) return;
64
+ const frame = clamp01(p) * a.totalFrames;
65
+ // goToAndStop parks the playhead — keep playing state instead.
66
+ if (a.isPaused) a.goToAndStop(frame, true);
67
+ else a.goToAndPlay(frame, true);
68
+ };
69
+
70
+ // Build once the container exists; rebuild on src/data/fit changes.
71
+ useEffect(() => {
72
+ const host = el.current;
73
+ if (!host) return;
74
+ build(host);
75
+ return () => {
76
+ anim.current?.destroy();
77
+ anim.current = null;
78
+ ready.current = false;
79
+ };
80
+ }, [props.src, props.data, props.fit]);
81
+
82
+ useEffect(() => {
83
+ if (props.loop !== undefined && anim.current) {
84
+ anim.current.loop = props.loop;
85
+ }
86
+ }, [props.loop]);
87
+
88
+ useEffect(() => {
89
+ if (props.playing === undefined || !anim.current || !ready.current) return;
90
+ if (props.playing) anim.current.play();
91
+ else anim.current.pause();
92
+ }, [props.playing]);
93
+
94
+ useEffect(() => {
95
+ if (props.progress === undefined) return;
96
+ seekTo(props.progress);
97
+ }, [props.progress]);
98
+
99
+ useEffect(() => {
100
+ if (props.speed !== undefined) anim.current?.setSpeed(props.speed);
101
+ }, [props.speed]);
102
+
103
+ const handle = (a: AnimationItem): LottieHandle => ({
104
+ play: () => a.play(),
105
+ pause: () => a.pause(),
106
+ stop: () => a.stop(),
107
+ seekTo,
108
+ setSpeed: (n: number) => a.setSpeed(n),
109
+ progress: () => (a.totalFrames ? a.currentRawFrame / a.totalFrames : 0),
110
+ duration: () => a.getDuration() * 1000,
111
+ isPlaying: () => !a.isPaused,
112
+ native: a,
113
+ });
114
+
115
+ <div
116
+ ref={(v: HTMLDivElement | null) => {
117
+ el.current = v;
118
+ if (!v) return;
119
+ applyEscapeProps(v, props);
120
+ if (anim.current) props.bind?.(handle(anim.current));
121
+ }}
122
+ id={props.id}
123
+ className={props.className}
124
+ {...layoutChildProps(props, props.style)}
125
+ role={props.accessibilityRole ?? 'img'}
126
+ aria-label={props.accessibilityLabel}
127
+ aria-hidden={props.accessible === false ? true : undefined}
128
+ aria-description={props.accessibilityHint}
129
+ aria-valuetext={props.accessibilityValue}
130
+ aria-disabled={props.accessibilityState?.disabled}
131
+ aria-selected={props.accessibilityState?.selected}
132
+ aria-checked={props.accessibilityState?.checked}
133
+ aria-live={props.accessibilityLiveRegion === 'none' ? 'off' : props.accessibilityLiveRegion}
134
+ />
135
+ }
@@ -0,0 +1,8 @@
1
+ /** NS `className` splits on ' ' only, and the universal driver applies the
2
+ * prop via `String(value)` — an array would arrive comma-joined and match
3
+ * no class at all. Native leaves normalize through cx() before handing
4
+ * className to an intrinsic. (The DOM renderer already flattens arrays,
5
+ * so web leaves don't need this.) */
6
+ export function cx(...parts: any[]): string {
7
+ return parts.flat(Infinity).filter(Boolean).join(' ')
8
+ }
@@ -0,0 +1,65 @@
1
+ import { isAndroid, isIOS } from '@nativescript/core'
2
+ import type { Role } from './props'
3
+
4
+ /** Apply only the active device's escape bag after the primitive's own props. */
5
+ export function applyEscapeProps(view: any, props: { ios?: any; android?: any }): void {
6
+ if (!view) {
7
+ return
8
+ }
9
+
10
+ const bag = isIOS ? props.ios : isAndroid ? props.android : undefined
11
+ if (bag) {
12
+ Object.assign(view, bag)
13
+ }
14
+ }
15
+
16
+ const NATIVE_ACCESSIBILITY_ROLES: Record<Role, string> = {
17
+ button: 'button',
18
+ link: 'link',
19
+ search: 'search',
20
+ image: 'image',
21
+ heading: 'header',
22
+ adjustable: 'adjustable',
23
+ summary: 'summary',
24
+ text: 'text',
25
+ none: 'none',
26
+ progressbar: 'progressBar',
27
+ checkbox: 'checkbox',
28
+ switch: 'switch',
29
+ radio: 'radioButton',
30
+ spinbutton: 'spinButton',
31
+ // NativeScript has no tab role; a tab is still an actionable button and
32
+ // its selected state is carried separately by accessibilityState.
33
+ tab: 'button',
34
+ }
35
+
36
+ /** Translate the shared/ARIA spelling to NativeScript's narrower role enum. */
37
+ export function nativeAccessibilityRole(role?: Role): string | undefined {
38
+ return role ? NATIVE_ACCESSIBILITY_ROLES[role] : undefined
39
+ }
40
+
41
+ /** NativeScript's AccessibilityState is a single enum value, unlike the
42
+ * web's independent ARIA booleans. Priority follows the strongest state. */
43
+ export function nativeAccessibilityState(state?: {
44
+ disabled?: boolean
45
+ selected?: boolean
46
+ checked?: boolean
47
+ }): string | undefined {
48
+ if (state?.disabled) {
49
+ return 'disabled'
50
+ }
51
+
52
+ if (state?.selected) {
53
+ return 'selected'
54
+ }
55
+
56
+ if (state?.checked === true) {
57
+ return 'checked'
58
+ }
59
+
60
+ if (state?.checked === false) {
61
+ return 'unchecked'
62
+ }
63
+
64
+ return undefined
65
+ }
@@ -0,0 +1,25 @@
1
+ /** Apply DOM escape props after the primitive's own props. Dash-named keys
2
+ * are attributes; other keys use the DOM property where available. */
3
+ export function applyEscapeProps(element: any, props: { web?: any }): void {
4
+ if (!element || !props.web) {
5
+ return
6
+ }
7
+
8
+ for (const [key, value] of Object.entries(props.web)) {
9
+ if (key.includes('-')) {
10
+ if (value == null || value === false) {
11
+ element.removeAttribute(key)
12
+ } else {
13
+ element.setAttribute(key, value === true ? '' : String(value))
14
+ }
15
+ } else if (key in element) {
16
+ try {
17
+ element[key] = value
18
+ } catch {
19
+ element.setAttribute(key, String(value))
20
+ }
21
+ } else if (value != null) {
22
+ element.setAttribute(key, String(value))
23
+ }
24
+ }
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Lottie } from './Lottie.tsrx'
2
+ export type { LottieEvent, LottieFit, LottieHandle, LottieProps } from './props'
@@ -0,0 +1,2 @@
1
+ export { Lottie } from './Lottie.web.tsrx'
2
+ export type { LottieEvent, LottieFit, LottieHandle, LottieProps } from './props'