diff --git a/app/components/Package/WeeklyDownloadStats.vue b/app/components/Package/WeeklyDownloadStats.vue index fa192bb187..74f3daa90b 100644 --- a/app/components/Package/WeeklyDownloadStats.vue +++ b/app/components/Package/WeeklyDownloadStats.vue @@ -7,6 +7,7 @@ import { OKLCH_NEUTRAL_FALLBACK, lightenOklch } from '~/utils/colors' import { applyBlocklistCorrection } from '~/utils/download-anomalies' import type { RepoRef } from '#shared/utils/git-providers' import type { VueUiSparklineConfig, VueUiSparklineDatasetItem } from 'vue-data-ui' +import { onKeyDown } from '@vueuse/core' const props = defineProps<{ packageName: string @@ -205,6 +206,110 @@ const dataset = computed(() => const lastDatapoint = computed(() => dataset.value.at(-1)?.period ?? '') +const isLoop = shallowRef(false) +const showPulse = shallowRef(true) +const keyboardShortcuts = useKeyboardShortcuts() + +const cheatCode = [ + 'arrowup', + 'arrowright', + 'arrowleft', + 'arrowup', + 'arrowleft', + 'arrowright', +] as const + +type CheatKey = (typeof cheatCode)[number] + +const easterEgg = shallowRef([]) +let resetTimeout: ReturnType | undefined +const easterEggResetDelay = 1500 + +function resetEasterEgg() { + easterEgg.value = [] + clearTimeout(resetTimeout) + resetTimeout = undefined +} + +function pushEasterEggKey(key: CheatKey) { + clearTimeout(resetTimeout) + resetTimeout = setTimeout(resetEasterEgg, easterEggResetDelay) + + const nextIndex = easterEgg.value.length + const expectedKey = cheatCode[nextIndex] + // Reset if the position is wrong + if (!expectedKey || expectedKey !== key) { + resetEasterEgg() + return + } + + easterEgg.value.push(key) + + // Match! reset & trigger + if (easterEgg.value.length === cheatCode.length) { + resetEasterEgg() + layEgg() + } +} + +onKeyDown( + 'ArrowUp', + e => { + if (!keyboardShortcuts.value) return + pushEasterEggKey('arrowup') + }, + { dedupe: true }, +) + +onKeyDown( + 'ArrowRight', + e => { + if (!keyboardShortcuts.value) return + pushEasterEggKey('arrowright') + }, + { dedupe: true }, +) + +onKeyDown( + 'ArrowLeft', + e => { + if (!keyboardShortcuts.value) return + pushEasterEggKey('arrowleft') + }, + { dedupe: true }, +) + +onBeforeUnmount(() => { + resetEasterEgg() + clearTimeout(eggPulseTimeout) + eggPulseTimeout = undefined +}) + +const eggPulse = ref(false) + +let eggPulseTimeout: ReturnType | undefined + +function playEggPulse() { + eggPulse.value = false + void document.documentElement.offsetHeight + eggPulse.value = true + + clearTimeout(eggPulseTimeout) + + eggPulseTimeout = setTimeout(() => { + eggPulse.value = false + }, 900) +} + +function layEgg() { + showPulse.value = false + nextTick(() => { + showPulse.value = true + isLoop.value = !isLoop.value + playEggPulse() + }) +} + const config = computed(() => { return { theme: 'dark', @@ -248,8 +353,8 @@ const config = computed(() => { line: { color: colors.value.borderHover, pulse: { - show: true, // the pulse will not show if prefers-reduced-motion (enforced by vue-data-ui) - loop: false, + show: showPulse.value, // the pulse will not show if prefers-reduced-motion (enforced by vue-data-ui) + loop: isLoop.value, radius: 1.5, color: pulseColor.value!, easing: 'ease-in-out', @@ -306,7 +411,10 @@ const config = computed(() => { -
+