fix TS issues
This commit is contained in:
parent
fc17d3b911
commit
f3375f02b1
5 changed files with 142 additions and 123 deletions
|
|
@ -9,6 +9,10 @@ import CompositionMarkerImage from './assets/marker.png?url';
|
|||
|
||||
The `ScrollerLottie` component plays Lottie animations. It uses the [dotLottie-web](https://developers.lottiefiles.com/docs/dotlottie-player/dotlottie-web/) library to render the animations.
|
||||
|
||||
## How to use .lottie files
|
||||
|
||||
LottieFiles is the official platform for creating and editing Lottie animations, and exporting them in the dotLottie format for smaller file sizes. The free version of LottieFiles has limited features, so [Bodymovin](https://exchange.adobe.com/apps/cc/12557/bodymovin) remains a popular, free way to export animations as JSON files. You can use the [LottieFiles converter](https://lottiefiles.com/tools/lottie-to-dotlottie) to convert JSON files to dotLottie or optimized dotLottie formats. This component is flexible and supports both dotLottie and JSON animation files.
|
||||
|
||||
## Basic demo
|
||||
|
||||
To use the `ScrollerLottie` component, import it and provide the animation source. The height defaults to `100lvh`, but you can adjust this to any valid CSS height value such as `1200px` or `200lvh` with the `height` prop.
|
||||
|
|
@ -27,7 +31,7 @@ With the graphics kit, you'll likely get your text and prop values from an Archi
|
|||
# ArchieML doc
|
||||
[blocks]
|
||||
type: lottie
|
||||
src: LottieFile
|
||||
src: LottieFile.lottie
|
||||
:end
|
||||
[]
|
||||
```
|
||||
|
|
@ -37,13 +41,14 @@ With the graphics kit, you'll likely get your text and prop values from an Archi
|
|||
```svelte
|
||||
<script lang="ts">
|
||||
import { ScrollerLottie } from '@reuters-graphics/graphics-components';
|
||||
import { assets } from '$app/paths';
|
||||
</script>
|
||||
|
||||
{#each content.blocks as block}
|
||||
<!-- Inside the content.blocks for loop... -->
|
||||
{#if block.type == 'lottie'}
|
||||
<ScrollerLottie
|
||||
src={`./data/${block.src}.lottie?url`}
|
||||
src={`${assets}/animations/${block.src}`}
|
||||
autoplay
|
||||
loop
|
||||
showDebugInfo
|
||||
|
|
@ -70,7 +75,7 @@ With the graphics kit, you'll likely get your text and prop values from an Archi
|
|||
# ArchieML doc
|
||||
[blocks]
|
||||
type: lottie
|
||||
src: LottieFile
|
||||
src: LottieFile.lottie
|
||||
marker: myMarker
|
||||
:end
|
||||
[]
|
||||
|
|
@ -81,13 +86,14 @@ With the graphics kit, you'll likely get your text and prop values from an Archi
|
|||
```svelte
|
||||
<script lang="ts">
|
||||
import { ScrollerLottie } from '@reuters-graphics/graphics-components';
|
||||
import { assets } from '$app/paths';
|
||||
</script>
|
||||
|
||||
{#each content.blocks as block}
|
||||
<!-- Inside the content.blocks for loop... -->
|
||||
{#if block.type == 'lottie'}
|
||||
<ScrollerLottie
|
||||
src={`./data/${block.src}.lottie?url`}
|
||||
src={`${assets}/animations/${block.src}`}
|
||||
marker={block.marker}
|
||||
autoplay
|
||||
loop
|
||||
|
|
@ -109,7 +115,7 @@ With the graphics kit, you'll likely get your text and prop values from an Archi
|
|||
# ArchieML doc
|
||||
[blocks]
|
||||
type: lottie
|
||||
src: LottieFile
|
||||
src: LottieFile.lottie
|
||||
[.segment]
|
||||
start: 0
|
||||
end: 20
|
||||
|
|
@ -123,13 +129,14 @@ With the graphics kit, you'll likely get your text and prop values from an Archi
|
|||
```svelte
|
||||
<script lang="ts">
|
||||
import { ScrollerLottie } from '@reuters-graphics/graphics-components';
|
||||
import { assets } from '$app/paths';
|
||||
</script>
|
||||
|
||||
{#each content.blocks as block}
|
||||
<!-- Inside the content.blocks for loop... -->
|
||||
{#if block.type == 'lottie'}
|
||||
<ScrollerLottie
|
||||
src={`./data/${block.src}.lottie?url`}
|
||||
src={`${assets}/animations/${block.src}`}
|
||||
segment={[block.segment.start, block.segment.end]}
|
||||
autoplay
|
||||
loop
|
||||
|
|
@ -151,7 +158,7 @@ With the graphics kit, you'll likely get your text and prop values from an Archi
|
|||
# ArchieML doc
|
||||
[blocks]
|
||||
type: lottie
|
||||
src: LottieFile
|
||||
src: LottieFile.lottie
|
||||
theme: myTheme
|
||||
:end
|
||||
[]
|
||||
|
|
@ -162,13 +169,14 @@ With the graphics kit, you'll likely get your text and prop values from an Archi
|
|||
```svelte
|
||||
<script lang="ts">
|
||||
import { ScrollerLottie } from '@reuters-graphics/graphics-components';
|
||||
import { assets } from '$app/paths';
|
||||
</script>
|
||||
|
||||
{#each content.blocks as block}
|
||||
<!-- Inside the content.blocks for loop... -->
|
||||
{#if block.type == 'lottie'}
|
||||
<ScrollerLottie
|
||||
src={`./data/${block.src}.lottie?url`}
|
||||
src={`${assets}/animations/${block.src}`}
|
||||
theme={block.theme}
|
||||
autoplay
|
||||
loop
|
||||
|
|
@ -270,7 +278,7 @@ With the graphics kit, you'll likely get your text and prop values from an Archi
|
|||
# ArchieML doc
|
||||
[blocks]
|
||||
type: lottie
|
||||
src: LottieFile
|
||||
src: LottieFile.lottie
|
||||
|
||||
# Array of foregrounds
|
||||
[.foregrounds]
|
||||
|
|
@ -303,7 +311,11 @@ With the graphics kit, you'll likely get your text and prop values from an Archi
|
|||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { ScrollerLottie } from '@reuters-graphics/graphics-components';
|
||||
import {
|
||||
ScrollerLottie,
|
||||
ScrollerLottieForeground,
|
||||
} from '@reuters-graphics/graphics-components';
|
||||
import { assets } from '$app/paths';
|
||||
|
||||
const Components = $state({
|
||||
Headline,
|
||||
|
|
@ -315,7 +327,7 @@ With the graphics kit, you'll likely get your text and prop values from an Archi
|
|||
<!-- Inside the content.blocks for loop... -->
|
||||
{#if block.type == 'lottie'}
|
||||
<ScrollerLottie
|
||||
src={`./data/${block.src}.lottie?url`}
|
||||
src={`${assets}/animations/${block.src}`}
|
||||
theme={block.theme}
|
||||
autoplay
|
||||
loop
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
speed?: Config['speed'];
|
||||
src?: Config['src'];
|
||||
useFrameInterpolation?: Config['useFrameInterpolation'];
|
||||
marker?: Config['marker'] | null | undefined;
|
||||
marker?: Config['marker'] | undefined;
|
||||
layout?: Config['layout'];
|
||||
animationId?: Config['animationId'];
|
||||
themeId?: Config['themeId'];
|
||||
|
|
@ -56,8 +56,8 @@
|
|||
let canvas: HTMLCanvasElement;
|
||||
let canvasWidth: number = $state(1);
|
||||
let canvasHeight: number = $state(1);
|
||||
let prevSrc = void 0;
|
||||
let prevData = void 0;
|
||||
let prevSrc: undefined | string = void 0;
|
||||
let prevData: undefined | unknown = void 0;
|
||||
let progressTween = new Tween(0, { duration: 100 });
|
||||
let start: number = $state(0);
|
||||
let end: number = $state(0);
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
themeId = '',
|
||||
themeData = '',
|
||||
playOnHover = false,
|
||||
marker = '',
|
||||
marker = undefined,
|
||||
layout = { fit: 'contain', align: [0.5, 0.5] },
|
||||
animationId = '',
|
||||
lottiePlayer = $bindable(undefined),
|
||||
|
|
@ -99,8 +99,7 @@
|
|||
setContext('lottieState', lottieState);
|
||||
|
||||
function onLoadEvent() {
|
||||
if (showDebugInfo) {
|
||||
// set layout
|
||||
if (lottiePlayer) {
|
||||
lottiePlayer.setLayout(layout);
|
||||
|
||||
lottieState.allMarkers = lottiePlayer.markers().map((x) => x.name);
|
||||
|
|
@ -116,7 +115,6 @@
|
|||
start = segment ? segment[0] : 0;
|
||||
end = segment ? segment[1] : lottiePlayer.totalFrames - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// set to frame 1 to trigger initial render
|
||||
// helpful especially when themeId is set
|
||||
|
|
@ -124,6 +122,7 @@
|
|||
|
||||
onLoad(); // call user-defined onLoad function
|
||||
}
|
||||
}
|
||||
|
||||
function onCompleteEvent() {
|
||||
onComplete();
|
||||
|
|
@ -152,23 +151,73 @@
|
|||
|
||||
if (lottiePlayer && lottieState) {
|
||||
keys.forEach((key) => {
|
||||
lottieState[key] = lottiePlayer[key];
|
||||
});
|
||||
switch (key) {
|
||||
case 'currentFrame':
|
||||
lottieState.currentFrame = lottiePlayer!.currentFrame;
|
||||
break;
|
||||
case 'totalFrames':
|
||||
lottieState.totalFrames = lottiePlayer!.totalFrames;
|
||||
break;
|
||||
case 'duration':
|
||||
lottieState.duration = lottiePlayer!.duration;
|
||||
break;
|
||||
case 'loop':
|
||||
lottieState.loop = lottiePlayer!.loop;
|
||||
break;
|
||||
case 'speed':
|
||||
lottieState.speed = lottiePlayer!.speed;
|
||||
break;
|
||||
case 'loopCount':
|
||||
lottieState.loopCount = lottiePlayer!.loopCount;
|
||||
break;
|
||||
case 'mode':
|
||||
lottieState.mode = lottiePlayer!.mode;
|
||||
break;
|
||||
case 'isPaused':
|
||||
lottieState.isPaused = lottiePlayer!.isPaused;
|
||||
break;
|
||||
case 'isPlaying':
|
||||
lottieState.isPlaying = lottiePlayer!.isPlaying;
|
||||
break;
|
||||
case 'isStopped':
|
||||
lottieState.isStopped = lottiePlayer!.isStopped;
|
||||
break;
|
||||
case 'isLoaded':
|
||||
lottieState.isLoaded = lottiePlayer!.isLoaded;
|
||||
break;
|
||||
case 'isFrozen':
|
||||
lottieState.isFrozen = lottiePlayer!.isFrozen;
|
||||
break;
|
||||
case 'segment':
|
||||
lottieState.segment = lottiePlayer!.segment ?? null;
|
||||
break;
|
||||
case 'autoplay':
|
||||
lottieState.autoplay = lottiePlayer!.autoplay ?? false;
|
||||
break;
|
||||
case 'layout':
|
||||
lottieState.layout = lottiePlayer!.layout ?? null;
|
||||
break;
|
||||
case 'activeThemeId':
|
||||
lottieState.activeThemeId = lottiePlayer!.activeThemeId ?? null;
|
||||
break;
|
||||
case 'marker':
|
||||
lottieState.marker = lottiePlayer!.marker ?? undefined;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
progress = (lottiePlayer.currentFrame + 1) / lottiePlayer.totalFrames;
|
||||
|
||||
lottieState.progress = progress;
|
||||
|
||||
onRender(); // call user-defined onRender function
|
||||
}
|
||||
}
|
||||
|
||||
const hoverHandler = (event) => {
|
||||
if (!playOnHover || !lottiePlayer.isLoaded) return;
|
||||
const hoverHandler = (event: MouseEvent) => {
|
||||
if (!playOnHover || !lottiePlayer?.isLoaded) return;
|
||||
if (event.type === 'mouseenter') {
|
||||
lottiePlayer.play();
|
||||
lottiePlayer?.play();
|
||||
} else if (event.type === 'mouseleave') {
|
||||
lottiePlayer.pause();
|
||||
lottiePlayer?.pause();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -216,7 +265,7 @@
|
|||
return () => {
|
||||
canvas.removeEventListener('mouseenter', hoverHandler);
|
||||
canvas.removeEventListener('mouseleave', hoverHandler);
|
||||
lottiePlayer.destroy();
|
||||
lottiePlayer?.destroy();
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -232,12 +281,12 @@
|
|||
$effect(() => {
|
||||
if (lottieState.isLoaded && lottieState.progress !== progress) {
|
||||
autoplay = false;
|
||||
lottiePlayer.pause();
|
||||
lottiePlayer?.pause();
|
||||
loop = false;
|
||||
|
||||
if (progress >= 0 && progress <= 1) {
|
||||
if (lottieState.isFrozen) {
|
||||
lottiePlayer.unfreeze();
|
||||
lottiePlayer?.unfreeze();
|
||||
lottieState.isFrozen = false;
|
||||
}
|
||||
const targetFrame = map(
|
||||
|
|
@ -258,7 +307,7 @@
|
|||
} else {
|
||||
progressTween.target = progress < 0 ? start : end;
|
||||
}
|
||||
lottiePlayer.freeze();
|
||||
lottiePlayer?.freeze();
|
||||
lottieState.isFrozen = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -267,7 +316,7 @@
|
|||
// Tweens to progress value
|
||||
$effect(() => {
|
||||
if (progressTween.current >= 0) {
|
||||
lottiePlayer.setFrame(progressTween.current);
|
||||
lottiePlayer?.setFrame(progressTween.current);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -275,7 +324,7 @@
|
|||
$effect(() => {
|
||||
if (
|
||||
typeof layout === 'object' &&
|
||||
lottiePlayer.isLoaded &&
|
||||
lottiePlayer?.isLoaded &&
|
||||
!isEqual(layout, lottiePlayer.layout)
|
||||
) {
|
||||
lottiePlayer.setLayout(layout);
|
||||
|
|
@ -284,15 +333,15 @@
|
|||
|
||||
// Handles marker change
|
||||
$effect(() => {
|
||||
if (lottieState.isLoaded && lottiePlayer.marker !== marker) {
|
||||
if (lottieState.isLoaded && lottiePlayer?.marker !== marker) {
|
||||
if (typeof marker === 'string') {
|
||||
lottiePlayer.setMarker(marker);
|
||||
lottiePlayer?.setMarker(marker);
|
||||
|
||||
start =
|
||||
lottiePlayer.markers().find((m) => m.name === marker)?.time ?? 0;
|
||||
lottiePlayer?.markers().find((m) => m.name === marker)?.time ?? 0;
|
||||
end =
|
||||
start +
|
||||
(lottiePlayer.markers().find((m) => m.name === marker)?.duration ??
|
||||
(lottiePlayer?.markers().find((m) => m.name === marker)?.duration ??
|
||||
0);
|
||||
|
||||
// change lottieState marker because
|
||||
|
|
@ -301,7 +350,7 @@
|
|||
lottieState.marker = marker;
|
||||
}
|
||||
} else if (marker === null || marker === undefined) {
|
||||
lottiePlayer.setMarker('');
|
||||
lottiePlayer?.setMarker('');
|
||||
} else {
|
||||
console.warn('Invalid marker type:', marker);
|
||||
}
|
||||
|
|
@ -313,9 +362,9 @@
|
|||
if (
|
||||
lottieState.isLoaded &&
|
||||
typeof speed == 'number' &&
|
||||
lottiePlayer.speed !== speed
|
||||
lottiePlayer?.speed !== speed
|
||||
) {
|
||||
lottiePlayer.setSpeed(speed);
|
||||
lottiePlayer?.setSpeed(speed);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -324,15 +373,15 @@
|
|||
if (
|
||||
lottieState.isLoaded &&
|
||||
typeof useFrameInterpolation == 'boolean' &&
|
||||
lottiePlayer.useFrameInterpolation !== useFrameInterpolation
|
||||
lottiePlayer?.useFrameInterpolation !== useFrameInterpolation
|
||||
) {
|
||||
lottiePlayer.setUseFrameInterpolation(useFrameInterpolation);
|
||||
lottiePlayer?.setUseFrameInterpolation(useFrameInterpolation);
|
||||
}
|
||||
});
|
||||
|
||||
// Handles segment change
|
||||
$effect(() => {
|
||||
if (lottieState.isLoaded && !isEqual(lottiePlayer.segment, segment)) {
|
||||
if (lottieState.isLoaded && !isEqual(lottiePlayer?.segment, segment)) {
|
||||
if (
|
||||
Array.isArray(segment) &&
|
||||
segment.length === 2 &&
|
||||
|
|
@ -340,9 +389,9 @@
|
|||
typeof segment[1] === 'number'
|
||||
) {
|
||||
let [start, end] = segment;
|
||||
lottiePlayer.setSegment(start, end);
|
||||
lottiePlayer?.setSegment(start, end);
|
||||
} else if (segment === null || segment === undefined) {
|
||||
lottiePlayer.setSegment(0, lottiePlayer.totalFrames);
|
||||
lottiePlayer?.setSegment(0, lottiePlayer?.totalFrames);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -352,9 +401,9 @@
|
|||
if (
|
||||
lottieState.isLoaded &&
|
||||
typeof loop == 'boolean' &&
|
||||
lottiePlayer.loop !== loop
|
||||
lottiePlayer?.loop !== loop
|
||||
) {
|
||||
lottiePlayer.setLoop(loop);
|
||||
lottiePlayer?.setLoop(loop);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -373,9 +422,9 @@
|
|||
$effect(() => {
|
||||
if (
|
||||
lottieState.isLoaded &&
|
||||
lottiePlayer.backgroundColor !== backgroundColor
|
||||
lottiePlayer?.backgroundColor !== backgroundColor
|
||||
) {
|
||||
lottiePlayer.setBackgroundColor(backgroundColor || '');
|
||||
lottiePlayer?.setBackgroundColor(backgroundColor || '');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -384,16 +433,16 @@
|
|||
if (
|
||||
lottieState.isLoaded &&
|
||||
typeof mode == 'string' &&
|
||||
lottiePlayer.mode !== mode
|
||||
lottiePlayer?.mode !== mode
|
||||
) {
|
||||
lottiePlayer.setMode(mode);
|
||||
lottiePlayer?.setMode(mode);
|
||||
}
|
||||
});
|
||||
|
||||
// Handles src change
|
||||
$effect(() => {
|
||||
if (lottieState && src !== prevSrc) {
|
||||
lottiePlayer.load({
|
||||
lottiePlayer?.load({
|
||||
src,
|
||||
autoplay,
|
||||
loop,
|
||||
|
|
@ -441,23 +490,23 @@
|
|||
$effect(() => {
|
||||
if (
|
||||
lottieState.isLoaded &&
|
||||
lottiePlayer.activeAnimationId !== animationId
|
||||
lottiePlayer?.activeAnimationId !== animationId
|
||||
) {
|
||||
lottiePlayer.loadAnimation(animationId);
|
||||
lottiePlayer?.loadAnimation(animationId);
|
||||
}
|
||||
});
|
||||
|
||||
// Handles themeId change
|
||||
$effect(() => {
|
||||
if (lottieState.isLoaded && lottiePlayer.activeThemeId !== themeId) {
|
||||
lottiePlayer.setTheme(themeId);
|
||||
if (lottieState.isLoaded && lottiePlayer?.activeThemeId !== themeId) {
|
||||
lottiePlayer?.setTheme(themeId);
|
||||
}
|
||||
});
|
||||
|
||||
// Handles themeData change
|
||||
$effect(() => {
|
||||
if (lottieState.isLoaded && lottiePlayer.isLoaded) {
|
||||
lottiePlayer.setThemeData(themeData);
|
||||
if (lottieState.isLoaded && lottiePlayer?.isLoaded) {
|
||||
lottiePlayer?.setThemeData(themeData);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
@ -495,27 +544,4 @@
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
z-index: 3;
|
||||
margin: 0;
|
||||
min-width: 25vmin;
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
padding: 4px 0 0 8px;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
p {
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding: 4px 8px 8px 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
// Types
|
||||
import type { Component, Snippet } from 'svelte';
|
||||
import type { LottieState } from './lottieState.svelte';
|
||||
import type { LottieState } from './ts/lottieState.svelte';
|
||||
import type {
|
||||
ContainerWidth,
|
||||
ScrollerLottieForegroundPosition,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,16 @@
|
|||
import type { Layout } from '@lottiefiles/dotlottie-web';
|
||||
|
||||
export interface LottieState {
|
||||
[key: string]:
|
||||
| number
|
||||
| boolean
|
||||
| string
|
||||
| null
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| [number, number]
|
||||
| Layout
|
||||
| undefined;
|
||||
progress: number;
|
||||
currentFrame: number;
|
||||
totalFrames: number;
|
||||
|
|
@ -14,9 +26,9 @@ export interface LottieState {
|
|||
isFrozen: boolean;
|
||||
segment: null | [number, number];
|
||||
autoplay: boolean;
|
||||
layout: null | string;
|
||||
layout: null | Layout;
|
||||
allMarkers: Array<string>;
|
||||
marker: null | string;
|
||||
marker: undefined | string;
|
||||
allThemes: Array<string>;
|
||||
activeThemeId: null | string;
|
||||
}
|
||||
|
|
@ -40,7 +52,7 @@ export function createLottieState(): LottieState {
|
|||
autoplay: false,
|
||||
layout: null,
|
||||
allMarkers: [],
|
||||
marker: null,
|
||||
marker: undefined,
|
||||
allThemes: [],
|
||||
activeThemeId: null,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,37 +34,6 @@
|
|||
<style lang="scss">
|
||||
@use '../../../scss/mixins' as mixins;
|
||||
|
||||
#progress-bar {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 0;
|
||||
padding: 1rem;
|
||||
|
||||
progress {
|
||||
height: 6px;
|
||||
background-color: #ff000044; /* Background color of the entire bar */
|
||||
}
|
||||
|
||||
progress::-webkit-progress-value {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-bar {
|
||||
background-color: #444444;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: var(--theme-font-family-sans-serif);
|
||||
color: white;
|
||||
font-size: var(--theme-font-size-xs);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.step-foreground-container {
|
||||
height: 100lvh;
|
||||
width: 50%;
|
||||
|
|
|
|||
Loading…
Reference in a new issue