Skip to content

Commit b9f18da

Browse files
authored
feat: add easter egg CPR for sparkline pulse (#1865)
1 parent 1b8421a commit b9f18da

File tree

1 file changed

+155
-3
lines changed

1 file changed

+155
-3
lines changed

app/components/Package/WeeklyDownloadStats.vue

Lines changed: 155 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,110 @@ const dataset = computed<VueUiSparklineDatasetItem[]>(() =>
205206
206207
const lastDatapoint = computed(() => dataset.value.at(-1)?.period ?? '')
207208
209+
const isLoop = shallowRef(false)
210+
const showPulse = shallowRef(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 = shallowRef<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+
const expectedKey = cheatCode[nextIndex]
240+
// Reset if the position is wrong
241+
if (!expectedKey || expectedKey !== 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+
clearTimeout(eggPulseTimeout)
285+
eggPulseTimeout = undefined
286+
})
287+
288+
const eggPulse = ref(false)
289+
290+
let eggPulseTimeout: ReturnType<typeof setTimeout> | undefined
291+
292+
function playEggPulse() {
293+
eggPulse.value = false
294+
void document.documentElement.offsetHeight
295+
eggPulse.value = true
296+
297+
clearTimeout(eggPulseTimeout)
298+
299+
eggPulseTimeout = setTimeout(() => {
300+
eggPulse.value = false
301+
}, 900)
302+
}
303+
304+
function layEgg() {
305+
showPulse.value = false
306+
nextTick(() => {
307+
showPulse.value = true
308+
isLoop.value = !isLoop.value
309+
playEggPulse()
310+
})
311+
}
312+
208313
const config = computed<VueUiSparklineConfig>(() => {
209314
return {
210315
theme: 'dark',
@@ -248,8 +353,8 @@ const config = computed<VueUiSparklineConfig>(() => {
248353
line: {
249354
color: colors.value.borderHover,
250355
pulse: {
251-
show: true, // the pulse will not show if prefers-reduced-motion (enforced by vue-data-ui)
252-
loop: false,
356+
show: showPulse.value, // the pulse will not show if prefers-reduced-motion (enforced by vue-data-ui)
357+
loop: isLoop.value,
253358
radius: 1.5,
254359
color: pulseColor.value!,
255360
easing: 'ease-in-out',
@@ -306,7 +411,10 @@ const config = computed<VueUiSparklineConfig>(() => {
306411
<span v-else-if="isLoadingWeeklyDownloads" class="min-w-6 min-h-6 -m-1 p-1" />
307412
</template>
308413

309-
<div class="w-full overflow-hidden h-[76px]">
414+
<div
415+
class="w-full overflow-hidden h-[76px] egg-pulse-target"
416+
:class="{ 'egg-pulse': eggPulse }"
417+
>
310418
<template v-if="isLoadingWeeklyDownloads || hasWeeklyDownloads">
311419
<ClientOnly>
312420
<VueUiSparkline class="w-full max-w-xs" :dataset :config>
@@ -402,4 +510,48 @@ const config = computed<VueUiSparklineConfig>(() => {
402510
Geist Mono,
403511
monospace !important;
404512
}
513+
514+
.egg-pulse-target {
515+
transform-origin: center;
516+
will-change: transform;
517+
}
518+
519+
.egg-pulse {
520+
animation: egg-heartbeat 900ms ease-in-out 0ms 1;
521+
}
522+
523+
/* 3 heart pulses */
524+
@keyframes egg-heartbeat {
525+
0% {
526+
transform: scale(1);
527+
}
528+
10% {
529+
transform: scale(1.1);
530+
}
531+
20% {
532+
transform: scale(1);
533+
}
534+
35% {
535+
transform: scale(1.03);
536+
}
537+
45% {
538+
transform: scale(1);
539+
}
540+
60% {
541+
transform: scale(1.01);
542+
}
543+
70% {
544+
transform: scale(1);
545+
}
546+
100% {
547+
transform: scale(1);
548+
}
549+
}
550+
551+
@media (prefers-reduced-motion: reduce) {
552+
.egg-pulse {
553+
animation: none !important;
554+
transform: none !important;
555+
}
556+
}
405557
</style>

0 commit comments

Comments
 (0)