0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-04 02:01:58 -05:00

Added animation to the analytics numbers when refreshed (#20842)

ref DES-709

- when refresh button is clicked, the numbers in the analytics will be animated if changed
- for the animation to be performant, added a new dependency "animejs"
- to minimize the flash and layout shift, the analytics data is kept as it is while loading
- once finished loading, it will be replaced with the new data
This commit is contained in:
Sodbileg Gansukh 2024-08-28 17:25:37 +08:00 committed by GitHub
parent 1d17600f5d
commit 1afe96ae34
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 365 additions and 126 deletions

View file

@ -41,8 +41,10 @@
@buttonText="Refresh"
@task={{this.fetchPostTask}}
@showIcon={{true}}
@idleIcon="reload"
@successText="Refreshed"
@class="gh-btn gh-btn-icon refresh" />
@class="gh-btn gh-btn-icon refresh"
@successClass="gh-btn gh-btn-icon refresh" />
{{/if}}
{{#unless this.post.emailOnly}}
<button type="button" class="gh-post-list-cta share" {{on "click" this.togglePublishFlowModal}}>
@ -99,7 +101,10 @@
<Tabs::Tabs class="gh-tabs-analytics {{if (eq this.post.hasBeenEmailed null) "no-tabs"}}" @forceRender={{true}} as |tabs|>
{{#if this.post.hasBeenEmailed}}
<tabs.tab>
<h3>{{svg-jar "analytics-tab-sent-large"}}{{format-number this.post.email.emailCount}}</h3>
<h3>
{{svg-jar "analytics-tab-sent-large"}}
<span class="animated-number sent" {{did-update this.applyClasses this.post.email.emailCount}}>{{split-number this.post.email.emailCount this.previousSentCount}}</span>
</h3>
<p>{{svg-jar "analytics-tab-sent"}}<span class="analytics-tab-label">Sent</span></p>
</tabs.tab>
@ -113,7 +118,10 @@
{{#if this.post.showEmailOpenAnalytics }}
<tabs.tab>
<h3>{{svg-jar "analytics-tab-opened-large"}}{{format-number this.post.email.openedCount}}</h3>
<h3>
{{svg-jar "analytics-tab-opened-large"}}
<span class="animated-number opened" {{did-update this.applyClasses this.post.email.openedCount}}>{{split-number this.post.email.openedCount this.previousOpenedCount}}</span>
</h3>
<p>{{svg-jar "analytics-tab-opened"}}<span class="analytics-tab-label">Opened<span class="analytics-tab-percentage">&nbsp;— {{this.post.email.openRate}}%</span></span></p>
</tabs.tab>
@ -128,7 +136,10 @@
{{#if this.post.showEmailClickAnalytics }}
<tabs.tab>
<h3>{{svg-jar "analytics-tab-clicked-large"}}{{format-number this.post.count.clicks}}</h3>
<h3>
{{svg-jar "analytics-tab-clicked-large"}}
<span class="animated-number clicked" {{did-update this.applyClasses this.post.count.clicks}}>{{split-number this.post.count.clicks this.previousClickedCount}}</span>
</h3>
<p>{{svg-jar "analytics-tab-clicked"}}<span class="analytics-tab-label">Clicked<span class="analytics-tab-percentage">&nbsp;— {{this.post.clickRate}}%</span></span></p>
</tabs.tab>
@ -143,7 +154,10 @@
{{#if this.post.isFeedbackEnabledForEmail }}
<tabs.tab>
<h3>{{svg-jar "analytics-tab-feedback-large"}}{{format-number this.totalFeedback}}</h3>
<h3>
{{svg-jar "analytics-tab-feedback-large"}}
<span class="animated-number feedback" {{did-update this.applyClasses this.totalFeedback}}>{{split-number this.totalFeedback this.previousFeedbackCount}}</span>
</h3>
<p>{{svg-jar "analytics-tab-feedback"}}<span class="analytics-tab-label">Feedback<span class="analytics-tab-percentage">&nbsp;— {{this.post.sentiment}}%</span></span></p>
</tabs.tab>
@ -162,7 +176,7 @@
<h3>
{{svg-jar "analytics-tab-conversions-large"}}
{{#if this.post.hasBeenEmailed}}
{{format-number this.post.count.conversions}}
<span class="animated-number conversions" {{did-update this.applyClasses this.post.count.conversions}}>{{split-number this.post.count.conversions this.previousConversionsCount}}</span>
{{else}}
Conversions
{{/if}}

View file

@ -1,6 +1,7 @@
import Component from '@glimmer/component';
import DeletePostModal from '../modals/delete-post';
import PostSuccessModal from '../modal-post-success';
import anime from 'animejs/lib/anime.es.js';
import {action} from '@ember/object';
import {didCancel, task} from 'ember-concurrency';
import {inject as service} from '@ember/service';
@ -39,6 +40,12 @@ export default class Analytics extends Component {
@tracked _post = null;
@tracked postCount = null;
@tracked showPostCount = false;
@tracked shouldAnimate = false;
@tracked previousSentCount = this.post.email.emailCount;
@tracked previousOpenedCount = this.post.email.openedCount;
@tracked previousClickedCount = this.post.count.clicks;
@tracked previousFeedbackCount = this.totalFeedback;
@tracked previousConversionsCount = this.post.count.conversions;
displayOptions = DISPLAY_OPTIONS;
constructor() {
@ -367,19 +374,61 @@ export default class Analytics extends Component {
@task
*fetchPostTask() {
const result = yield this.store.query('post', {filter: `id:${this.post.id}`, include: 'count.clicks,count.conversions,count.paid_conversions,count.positive_feedback,count.negative_feedback', limit: 1});
const currentSentCount = this.post.email.emailCount;
const currentOpenedCount = this.post.email.openedCount;
const currentClickedCount = this.post.count.clicks;
const currentFeedbackCount = this.totalFeedback;
const currentConversionsCount = this.post.count.conversions;
this.shouldAnimate = true;
const result = yield this.store.query('post', {filter: `id:${this.post.id}`, include: 'email,count.clicks,count.conversions,count.positive_feedback,count.negative_feedback', limit: 1});
this.post = result.toArray()[0];
if (this.post.email) {
this.notifications.showNotification('Post analytics refreshing', {
description: 'It can take up to five minutes for all data to show.',
type: 'success'
});
}
this.previousSentCount = currentSentCount;
this.previousOpenedCount = currentOpenedCount;
this.previousClickedCount = currentClickedCount;
this.previousFeedbackCount = currentFeedbackCount;
this.previousConversionsCount = currentConversionsCount;
yield this.fetchLinks();
return true;
}
@action
applyClasses(element) {
if (!this.shouldAnimate ||
(element.classList.contains('sent') && this.post.email.emailCount === this.previousSentCount) ||
(element.classList.contains('opened') && this.post.email.openedCount === this.previousOpenedCount) ||
(element.classList.contains('clicked') && this.post.count.clicks === this.previousClickedCount) ||
(element.classList.contains('feedback') && this.totalFeedback === this.previousFeedbackCount) ||
(element.classList.contains('conversions') && this.post.count.conversions === this.previousConversionsCount)
) {
return;
}
anime({
targets: `${Array.from(element.classList).map(className => `.${className}`).join('')} .new-number span`,
translateY: [10,0],
// translateZ: 0,
opacity: [0,1],
easing: 'easeOutElastic',
elasticity: 650,
duration: 1000,
delay: (el, i) => 100 + 30 * i
});
anime({
targets: `${Array.from(element.classList).map(className => `.${className}`).join('')} .old-number span`,
translateY: [0,-10],
opacity: [1,0],
easing: 'easeOutExpo',
duration: 400,
delay: (el, i) => 100 + 10 * i
});
}
get showLinks() {
return this.post.showEmailClickAnalytics;
}

View file

@ -1,5 +1,7 @@
<div class="gh-post-activity-feed">
{{#let (activity-feed-fetcher filter=(members-event-filter post=@post.id includeEvents=this.getEventTypes) pageSize=this.pageSize) as |eventsFetcher|}}
{{compute (fn this.saveEvents eventsFetcher)}}
{{#if eventsFetcher.isError}}
<div class="gh-dashboard-list-body">
<div class="gh-dashboard-list-error">
@ -11,129 +13,232 @@
</div>
{{/if}}
{{#if eventsFetcher.data}}
<div class="gh-dashboard-list-body gh-dashboard-list-cols-{{this.eventType}}">
{{#each eventsFetcher.data as |event|}}
{{#let (parse-member-event event) as |parsedEvent|}}
<div class="gh-dashboard-list-item">
<div class="gh-dashboard-list-item-sub">
{{#if parsedEvent.member}}
<GhMemberAvatar @member={{parsedEvent.member}} @containerClass="w6 h6 mr3 flex-shrink-0" />
<LinkTo class="gh-dashboard-list-text" @route="member" @model="{{parsedEvent.memberId}}" @query={{hash postAnalytics=@post.id}}>{{parsedEvent.subject}}</LinkTo>
{{else}}
{{#if parsedEvent.email}}
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w6 h6 mr3 flex-shrink-0" />
<a class="gh-dashboard-list-text" href="mailto:{{parsedEvent.email}}" target="_blank" rel="noopener noreferrer" title="{{parsedEvent.email}}">{{parsedEvent.subject}}</a>
{{else}}
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w6 h6 mr3 flex-shrink-0" />
<span class="gh-dashboard-list-text">{{parsedEvent.subject}}</span>
{{/if}}
{{/if}}
</div>
<div class="gh-dashboard-list-item-sub">
{{svg-jar parsedEvent.icon }}
<span class="gh-dashboard-list-subtext">
<span class="gh-members-activity-description">
<span class="gh-members-activity-event-text">{{capitalize-first-letter parsedEvent.action}}</span>
{{#if parsedEvent.info}}
<span class="highlight">&nbsp;({{parsedEvent.info}})</span>
{{/if}}
</span>
</span>
</div>
{{#if (eq this.eventType "conversion")}}
{{#if eventsFetcher.isLoading}}
{{#if this.savedEventsFetcher.data}}
<div class="gh-dashboard-list-body gh-dashboard-list-cols-{{this.eventType}}">
{{#each this.savedEventsFetcher.data as |event|}}
{{#let (parse-member-event event) as |parsedEvent|}}
<div class="gh-dashboard-list-item">
<div class="gh-dashboard-list-item-sub">
{{#if parsedEvent.source}}
<span title="Source" class="gh-members-activity-description">{{svg-jar "event-extras-source"}}<span class="gh-members-activity-event-text">{{parsedEvent.source.name}}</span></span>
{{#if parsedEvent.member}}
<GhMemberAvatar @member={{parsedEvent.member}} @containerClass="w6 h6 mr3 flex-shrink-0" />
<LinkTo class="gh-dashboard-list-text" @route="member" @model="{{parsedEvent.memberId}}" @query={{hash postAnalytics=@post.id}}>{{parsedEvent.subject}}</LinkTo>
{{else}}
<span class="midlightgrey">&mdash;</span>
{{#if parsedEvent.email}}
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w6 h6 mr3 flex-shrink-0" />
<a class="gh-dashboard-list-text" href="mailto:{{parsedEvent.email}}" target="_blank" rel="noopener noreferrer" title="{{parsedEvent.email}}">{{parsedEvent.subject}}</a>
{{else}}
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w6 h6 mr3 flex-shrink-0" />
<span class="gh-dashboard-list-text">{{parsedEvent.subject}}</span>
{{/if}}
{{/if}}
</div>
<div class="gh-dashboard-list-item-sub">
{{svg-jar parsedEvent.icon }}
<span class="gh-dashboard-list-subtext">
<span class="gh-members-activity-description">
<span class="gh-members-activity-event-text">{{capitalize-first-letter parsedEvent.action}}</span>
{{#if parsedEvent.info}}
<span class="highlight">&nbsp;({{parsedEvent.info}})</span>
{{/if}}
</span>
</span>
</div>
{{#if (eq this.eventType "conversion")}}
<div class="gh-dashboard-list-item-sub">
{{#if parsedEvent.source}}
<span title="Source" class="gh-members-activity-description">{{svg-jar "event-extras-source"}}<span class="gh-members-activity-event-text">{{parsedEvent.source.name}}</span></span>
{{else}}
<span class="midlightgrey">&mdash;</span>
{{/if}}
</div>
{{/if}}
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-list-subtext">{{moment-from-now parsedEvent.timestamp}}</span>
</div>
</div>
{{/let}}
{{/each}}
{{#if (compute (fn this.areStubsNeeded eventsFetcher))}}
{{#let (compute (fn this.getAmountOfStubs eventsFetcher)) as |stubs|}}
{{#each stubs}}
<div class="gh-dashboard-list-item gh-dashboard-list-item-stub"></div>
{{/each}}
{{/let}}
{{/if}}
<div class="gh-post-activity-feed-footer">
<Posts::PostActivityFeed::FooterLinks
@eventType={{this.eventType}}
@post={{@post}}
/>
<div class="gh-post-activity-feed-pagination">
{{#if (compute (fn this.isPaginationNotNeeded this.savedEventsFetcher))}}
Showing {{this.savedEventsFetcher.totalEvents}} in total
{{else}}
Showing {{this.savedEventsFetcher.previousEvents}}-{{this.savedEventsFetcher.shownEvents}} of {{this.savedEventsFetcher.totalEvents}}
<div class="gh-post-activity-feed-pagination-group">
<button
class="gh-post-activity-feed-pagination-button gh-post-activity-feed-prev-button"
type="button"
title="Previous page"
disabled={{compute (fn this.isPreviousButtonDisabled this.savedEventsFetcher)}}
{{on "click" this.savedEventsFetcher.loadPreviousPage}}
>
{{svg-jar "arrow-left-pagination"}}
</button>
<button
class="gh-post-activity-feed-pagination-button gh-post-activity-feed-next-button"
type="button"
title="Next page"
disabled={{compute (fn this.isNextButtonDisabled this.savedEventsFetcher)}}
{{on "click" this.savedEventsFetcher.loadNextPage}}
>
{{svg-jar "arrow-right-pagination"}}
</button>
</div>
{{/if}}
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-list-subtext">{{moment-from-now parsedEvent.timestamp}}</span>
</div>
</div>
{{/let}}
{{/each}}
{{#if (compute (fn this.areStubsNeeded eventsFetcher))}}
{{#let (compute (fn this.getAmountOfStubs eventsFetcher)) as |stubs|}}
{{#each stubs}}
<div class="gh-dashboard-list-item gh-dashboard-list-item-stub"></div>
{{/each}}
{{/let}}
{{/if}}
<div class="gh-post-activity-feed-footer">
<Posts::PostActivityFeed::FooterLinks
@eventType={{this.eventType}}
@post={{@post}}
/>
<div class="gh-post-activity-feed-pagination">
{{#if (compute (fn this.isPaginationNotNeeded eventsFetcher))}}
Showing {{eventsFetcher.totalEvents}} in total
{{else}}
Showing {{eventsFetcher.previousEvents}}-{{eventsFetcher.shownEvents}} of {{eventsFetcher.totalEvents}}
<div class="gh-post-activity-feed-pagination-group">
<button
class="gh-post-activity-feed-pagination-button gh-post-activity-feed-prev-button"
type="button"
title="Previous page"
disabled={{compute (fn this.isPreviousButtonDisabled eventsFetcher)}}
{{on "click" eventsFetcher.loadPreviousPage}}
>
{{svg-jar "arrow-left-pagination"}}
</button>
<button
class="gh-post-activity-feed-pagination-button gh-post-activity-feed-next-button"
type="button"
title="Next page"
disabled={{compute (fn this.isNextButtonDisabled eventsFetcher)}}
{{on "click" eventsFetcher.loadNextPage}}
>
{{svg-jar "arrow-right-pagination"}}
</button>
</div>
{{/if}}
</div>
</div>
</div>
{{#if (eq @eventType 'feedback')}}
<Posts::FeedbackEventsChart @data={{@data}} />
{{else}}
<div class="gh-post-activity-feed-empty">
<div class="gh-post-analytics-loading">
<div class="gh-loading-spinner-outer">
<div class="gh-loading-spinner"></div>
</div>
</div>
</div>
{{/if}}
{{else}}
<div class="gh-dashboard-list-body">
<div class="gh-post-activity-feed-empty">
<div class="attribution-list-empty">
{{#if (eq this.eventType "sent")}}
{{svg-jar "empty-sent"}}
<h4>No members have received your email yet</h4>
<p>Once someone receives your email, you'll be able to see the member activity here.</p>
{{else if (eq this.eventType "opened")}}
{{svg-jar "empty-opened"}}
<h4>No members have opened your newsletter</h4>
<p>Once someone opens, you'll see them listed here.</p>
{{else if (eq this.eventType "clicked")}}
{{svg-jar "empty-clicked"}}
<h4>No links have been clicked in your newsletter</h4>
<p>Once a member clicks a link, you'll see them listed here.</p>
{{else if (eq this.eventType "feedback")}}
{{svg-jar "empty-feedback"}}
<h4>No members have given feedback yet</h4>
<p>When someone does, you'll see their response here.</p>
{{else if (eq this.eventType "conversion")}}
{{svg-jar "empty-conversion"}}
<h4>No members have signed up on this post</h4>
<p>When someone new signs up, you'll see them here.</p>
{{/if}}
{{#if eventsFetcher.data}}
<div class="gh-dashboard-list-body gh-dashboard-list-cols-{{this.eventType}}">
{{#each eventsFetcher.data as |event|}}
{{#let (parse-member-event event) as |parsedEvent|}}
<div class="gh-dashboard-list-item">
<div class="gh-dashboard-list-item-sub">
{{#if parsedEvent.member}}
<GhMemberAvatar @member={{parsedEvent.member}} @containerClass="w6 h6 mr3 flex-shrink-0" />
<LinkTo class="gh-dashboard-list-text" @route="member" @model="{{parsedEvent.memberId}}" @query={{hash postAnalytics=@post.id}}>{{parsedEvent.subject}}</LinkTo>
{{else}}
{{#if parsedEvent.email}}
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w6 h6 mr3 flex-shrink-0" />
<a class="gh-dashboard-list-text" href="mailto:{{parsedEvent.email}}" target="_blank" rel="noopener noreferrer" title="{{parsedEvent.email}}">{{parsedEvent.subject}}</a>
{{else}}
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w6 h6 mr3 flex-shrink-0" />
<span class="gh-dashboard-list-text">{{parsedEvent.subject}}</span>
{{/if}}
{{/if}}
</div>
<div class="gh-dashboard-list-item-sub">
{{svg-jar parsedEvent.icon }}
<span class="gh-dashboard-list-subtext">
<span class="gh-members-activity-description">
<span class="gh-members-activity-event-text">{{capitalize-first-letter parsedEvent.action}}</span>
{{#if parsedEvent.info}}
<span class="highlight">&nbsp;({{parsedEvent.info}})</span>
{{/if}}
</span>
</span>
</div>
{{#if (eq this.eventType "conversion")}}
<div class="gh-dashboard-list-item-sub">
{{#if parsedEvent.source}}
<span title="Source" class="gh-members-activity-description">{{svg-jar "event-extras-source"}}<span class="gh-members-activity-event-text">{{parsedEvent.source.name}}</span></span>
{{else}}
<span class="midlightgrey">&mdash;</span>
{{/if}}
</div>
{{/if}}
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-list-subtext">{{moment-from-now parsedEvent.timestamp}}</span>
</div>
</div>
{{/let}}
{{/each}}
{{#if (compute (fn this.areStubsNeeded eventsFetcher))}}
{{#let (compute (fn this.getAmountOfStubs eventsFetcher)) as |stubs|}}
{{#each stubs}}
<div class="gh-dashboard-list-item gh-dashboard-list-item-stub"></div>
{{/each}}
{{/let}}
{{/if}}
<div class="gh-post-activity-feed-footer">
<Posts::PostActivityFeed::FooterLinks
@eventType={{this.eventType}}
@post={{@post}}
/>
<div class="gh-post-activity-feed-pagination">
{{#if (compute (fn this.isPaginationNotNeeded eventsFetcher))}}
Showing {{eventsFetcher.totalEvents}} in total
{{else}}
Showing {{eventsFetcher.previousEvents}}-{{eventsFetcher.shownEvents}} of {{eventsFetcher.totalEvents}}
<div class="gh-post-activity-feed-pagination-group">
<button
class="gh-post-activity-feed-pagination-button gh-post-activity-feed-prev-button"
type="button"
title="Previous page"
disabled={{compute (fn this.isPreviousButtonDisabled eventsFetcher)}}
{{on "click" eventsFetcher.loadPreviousPage}}
>
{{svg-jar "arrow-left-pagination"}}
</button>
<button
class="gh-post-activity-feed-pagination-button gh-post-activity-feed-next-button"
type="button"
title="Next page"
disabled={{compute (fn this.isNextButtonDisabled eventsFetcher)}}
{{on "click" eventsFetcher.loadNextPage}}
>
{{svg-jar "arrow-right-pagination"}}
</button>
</div>
{{/if}}
</div>
</div>
</div>
</div>
{{#if (eq @eventType 'feedback')}}
<Posts::FeedbackEventsChart @data={{@data}} />
{{/if}}
{{else}}
<div class="gh-dashboard-list-body">
<div class="gh-post-activity-feed-empty">
<div class="attribution-list-empty">
{{#if (eq this.eventType "sent")}}
{{svg-jar "empty-sent"}}
<h4>No members have received your email yet</h4>
<p>Once someone receives your email, you'll be able to see the member activity here.</p>
{{else if (eq this.eventType "opened")}}
{{svg-jar "empty-opened"}}
<h4>No members have opened your newsletter</h4>
<p>Once someone opens, you'll see them listed here.</p>
{{else if (eq this.eventType "clicked")}}
{{svg-jar "empty-clicked"}}
<h4>No links have been clicked in your newsletter</h4>
<p>Once a member clicks a link, you'll see them listed here.</p>
{{else if (eq this.eventType "feedback")}}
{{svg-jar "empty-feedback"}}
<h4>No members have given feedback yet</h4>
<p>When someone does, you'll see their response here.</p>
{{else if (eq this.eventType "conversion")}}
{{svg-jar "empty-conversion"}}
<h4>No members have signed up on this post</h4>
<p>When someone new signs up, you'll see them here.</p>
{{/if}}
</div>
</div>
</div>
{{/if}}
{{/if}}
{{/let}}
</div>

View file

@ -1,6 +1,7 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class PostActivityFeed extends Component {
@service feature;
@ -14,6 +15,16 @@ export default class PostActivityFeed extends Component {
conversion: ['subscription_event', 'signup_event']
};
@tracked savedEventsFetcher = null;
@action
saveEvents(fetcher) {
if (fetcher && fetcher.data && fetcher.data.length > 0) {
this.savedEventsFetcher = fetcher;
}
return null;
}
get getEventTypes() {
return this._eventTypes[this.args.eventType];
}

View file

@ -0,0 +1,19 @@
import {formatNumber} from './format-number';
import {helper} from '@ember/component/helper';
import {htmlSafe} from '@ember/template';
export function splitNumber([number, previousNumber]) {
let formattedNewNumber = formatNumber(number);
let formattedOldNumber = formatNumber(previousNumber);
let oldChars = formattedOldNumber.split('').map(char => `<span class="old-char">${char}</span>`).join('');
let newChars = formattedNewNumber.split('').map(char => `<span class="new-char">${char}</span>`).join('');
return htmlSafe(`
<div class="number-group old-number">${oldChars}</div>
<div class="number-group new-number">${newChars}</div>
`);
}
export default helper(splitNumber);

View file

@ -1500,6 +1500,21 @@
margin-top: -2px;
}
.gh-post-analytics-header .gh-btn.refresh {
border: 1px solid var(--whitegrey-d1);
background: var(--white);
color: var(--darkgrey);
}
.gh-post-analytics-header .gh-btn.refresh:hover {
border-color: var(--lightgrey-l2);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
.gh-post-analytics-header .gh-btn.refresh svg path {
stroke-width: 2.25;
}
.gh-post-list-cta.is-hovered {
border-color: var(--whitegrey-d2);
}
@ -1532,7 +1547,7 @@
}
.gh-post-list-cta > svg path {
stroke-width: 2;
stroke-width: 1.75;
}
.gh-post-list-cta > span {
@ -1727,6 +1742,26 @@ span.dropdown .gh-post-list-cta > span {
height: 16px;
}
.gh-tabs-analytics .animated-number {
position: relative;
}
.gh-tabs-analytics .animated-number .new-number {
position: absolute;
top: 0;
left: 0;
}
.gh-tabs-analytics .animated-number .new-char,
.gh-tabs-analytics .animated-number .old-char {
display: inline-block;
}
.gh-tabs-analytics .animated-number .new-char {
opacity: 0;
transform: translateY(10px);
}
.gh-tabs-analytics .gh-dashboard-list-item {
grid-template-columns: 40% 40% 20%;
}

View file

@ -56,6 +56,7 @@
"@tryghost/nql-lang": "0.6.1",
"@tryghost/string": "0.2.12",
"@tryghost/timezone-data": "0.4.3",
"animejs": "3.2.2",
"autoprefixer": "9.8.6",
"babel-eslint": "10.1.0",
"babel-plugin-transform-class-properties": "6.24.1",
@ -205,4 +206,4 @@
}
}
}
}
}

View file

@ -9473,6 +9473,11 @@ amperize@0.6.1:
uuid "^7.0.1"
validator "^12.0.0"
animejs@3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/animejs/-/animejs-3.2.2.tgz#59be98c58834339d5847f4a70ddba74ac75b6afc"
integrity sha512-Ao95qWLpDPXXM+WrmwcKbl6uNlC5tjnowlaRYtuVDHHoygjtIPfDUoK9NthrlZsQSKjZXlmji2TrBUAVbiH0LQ==
ansi-colors@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"