@@ -7,6 +7,7 @@ import { OKLCH_NEUTRAL_FALLBACK, lightenOklch } from '~/utils/colors'
77import { applyBlocklistCorrection } from ' ~/utils/download-anomalies'
88import type { RepoRef } from ' #shared/utils/git-providers'
99import type { VueUiSparklineConfig , VueUiSparklineDatasetItem } from ' vue-data-ui'
10+ import { onKeyDown } from ' @vueuse/core'
1011
1112const props = defineProps <{
1213 packageName: string
@@ -205,6 +206,104 @@ const dataset = computed<VueUiSparklineDatasetItem[]>(() =>
205206
206207const lastDatapoint = computed (() => dataset .value .at (- 1 )?.period ?? ' ' )
207208
209+ const isLoop = ref (false )
210+ const showPulse = ref (true )
211+ const keyboardShortcuts = useKeyboardShortcuts ()
212+
213+ const cheatCode = [
214+ ' arrowup' ,
215+ ' arrowright' ,
216+ ' arrowleft' ,
217+ ' arrowup' ,
218+ ' arrowleft' ,
219+ ' arrowright' ,
220+ ] as const
221+
222+ type CheatKey = (typeof cheatCode )[number ]
223+
224+ const easterEgg = ref <CheatKey []>([])
225+ let resetTimeout: ReturnType <typeof setTimeout > | undefined
226+ const easterEggResetDelay = 1500
227+
228+ function resetEasterEgg() {
229+ easterEgg .value = []
230+ clearTimeout (resetTimeout )
231+ resetTimeout = undefined
232+ }
233+
234+ function pushEasterEggKey(key : CheatKey ) {
235+ clearTimeout (resetTimeout )
236+ resetTimeout = setTimeout (resetEasterEgg , easterEggResetDelay )
237+
238+ const nextIndex = easterEgg .value .length
239+
240+ // Reset if the position is wrong
241+ if (cheatCode [nextIndex ] !== key ) {
242+ resetEasterEgg ()
243+ return
244+ }
245+
246+ easterEgg .value .push (key )
247+
248+ // Match! reset & trigger
249+ if (easterEgg .value .length === cheatCode .length ) {
250+ resetEasterEgg ()
251+ layEgg ()
252+ }
253+ }
254+
255+ onKeyDown (
256+ ' ArrowUp' ,
257+ e => {
258+ if (! keyboardShortcuts .value ) return
259+ pushEasterEggKey (' arrowup' )
260+ },
261+ { dedupe: true },
262+ )
263+
264+ onKeyDown (
265+ ' ArrowRight' ,
266+ e => {
267+ if (! keyboardShortcuts .value ) return
268+ pushEasterEggKey (' arrowright' )
269+ },
270+ { dedupe: true },
271+ )
272+
273+ onKeyDown (
274+ ' ArrowLeft' ,
275+ e => {
276+ if (! keyboardShortcuts .value ) return
277+ pushEasterEggKey (' arrowleft' )
278+ },
279+ { dedupe: true },
280+ )
281+
282+ onBeforeUnmount (() => {
283+ resetEasterEgg ()
284+ })
285+
286+ const eggPulse = ref (false )
287+
288+ function playEggPulse() {
289+ eggPulse .value = false
290+ void document .documentElement .offsetHeight
291+ eggPulse .value = true
292+
293+ window .setTimeout (() => {
294+ eggPulse .value = false
295+ }, 900 )
296+ }
297+
298+ function layEgg() {
299+ showPulse .value = false
300+ nextTick (() => {
301+ showPulse .value = true
302+ isLoop .value = ! isLoop .value
303+ playEggPulse ()
304+ })
305+ }
306+
208307const config = computed <VueUiSparklineConfig >(() => {
209308 return {
210309 theme: ' dark' ,
@@ -248,8 +347,8 @@ const config = computed<VueUiSparklineConfig>(() => {
248347 line: {
249348 color: colors .value .borderHover ,
250349 pulse: {
251- show: true , // the pulse will not show if prefers-reduced-motion (enforced by vue-data-ui)
252- loop: false ,
350+ show: showPulse . value , // the pulse will not show if prefers-reduced-motion (enforced by vue-data-ui)
351+ loop: isLoop . value ,
253352 radius: 1.5 ,
254353 color: pulseColor .value ! ,
255354 easing: ' ease-in-out' ,
@@ -306,7 +405,10 @@ const config = computed<VueUiSparklineConfig>(() => {
306405 <span v-else-if =" isLoadingWeeklyDownloads" class =" min-w-6 min-h-6 -m-1 p-1" />
307406 </template >
308407
309- <div class =" w-full overflow-hidden h-[76px]" >
408+ <div
409+ class =" w-full overflow-hidden h-[76px] egg-pulse-target"
410+ :class =" { 'egg-pulse': eggPulse }"
411+ >
310412 <template v-if =" isLoadingWeeklyDownloads || hasWeeklyDownloads " >
311413 <ClientOnly >
312414 <VueUiSparkline class =" w-full max-w-xs" :dataset :config >
@@ -402,4 +504,48 @@ const config = computed<VueUiSparklineConfig>(() => {
402504 Geist Mono,
403505 monospace !important ;
404506}
507+
508+ .egg-pulse-target {
509+ transform-origin : center ;
510+ will-change : transform;
511+ }
512+
513+ .egg-pulse {
514+ animation : egg-heartbeat 900ms ease-in-out 0ms 1 ;
515+ }
516+
517+ /* 3 heart pulses */
518+ @keyframes egg-heartbeat {
519+ 0% {
520+ transform : scale (1 );
521+ }
522+ 10% {
523+ transform : scale (1.1 );
524+ }
525+ 20% {
526+ transform : scale (1 );
527+ }
528+ 35% {
529+ transform : scale (1.03 );
530+ }
531+ 45% {
532+ transform : scale (1 );
533+ }
534+ 60% {
535+ transform : scale (1.01 );
536+ }
537+ 70% {
538+ transform : scale (1 );
539+ }
540+ 100% {
541+ transform : scale (1 );
542+ }
543+ }
544+
545+ @media (prefers-reduced-motion: reduce) {
546+ .egg-pulse {
547+ animation : none !important ;
548+ transform : none !important ;
549+ }
550+ }
405551 </style >
0 commit comments