Skip to content

Commit d4f51ee

Browse files
committed
feat: add easter egg CPR for sparkline pulse
1 parent 970d141 commit d4f51ee

1 file changed

Lines changed: 149 additions & 3 deletions

File tree

app/components/Package/WeeklyDownloadStats.vue

Lines changed: 149 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { OKLCH_NEUTRAL_FALLBACK, lightenOklch } from '~/utils/colors'
77
import { applyBlocklistCorrection } from '~/utils/download-anomalies'
88
import type { RepoRef } from '#shared/utils/git-providers'
99
import type { VueUiSparklineConfig, VueUiSparklineDatasetItem } from 'vue-data-ui'
10+
import { onKeyDown } from '@vueuse/core'
1011
1112
const props = defineProps<{
1213
packageName: string
@@ -205,6 +206,104 @@ const dataset = computed<VueUiSparklineDatasetItem[]>(() =>
205206
206207
const 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+
208307
const 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

Comments
 (0)