Skip to content
Snippets Groups Projects
ACircularProgress.vue 2.53 KiB
Newer Older
  • Learn to ignore specific revisions
  • <template>
      <div
        :aria-valuemax="max"
        :aria-valuemin="min"
        :aria-valuenow="progress !== 'indeterminate' ? progress : undefined"
        :aria-valuetext="
          percentage ? `${percentage.toLocaleString(locale, { maximumFractionDigits: 2 })}%` : undefined
        "
        role="progressbar"
      >
        <svg
          class="tw-aspect-square tw-max-w-full tw-max-h-full"
          height="120"
          viewBox="0 0 120 120"
          width="120"
          xmlns="http://www.w3.org/2000/svg"
        >
          <g>
            <g class="circle" :class="{ indeterminate: progress === 'indeterminate' }">
              <circle class="track" cx="60" cy="60" fill="none" r="54" stroke-width="12" />
              <circle
                class="value"
                :stroke-dashoffset="dashOffset"
                cx="60"
                cy="60"
                fill="none"
                pathLength="100"
                r="54"
                stroke="currentColor"
                stroke-dasharray="100"
                stroke-width="12"
              />
            </g>
            <text
              v-if="percentage"
              x="50%"
              y="52.5%"
              dominant-baseline="middle"
              text-anchor="middle"
              font-size="200%"
            >
              {{ `${Math.round(percentage)}%` }}
            </text>
          </g>
        </svg>
      </div>
    </template>
    
    <script lang="ts" setup>
    import { computed } from 'vue'
    import { useI18n } from '@/i18n'
    
    const progress = defineModel<number | 'indeterminate'>('progress', { required: true })
    const props = withDefaults(
      defineProps<{
        min?: number
        max?: number
      }>(),
      {
        min: 0,
        max: 100,
      },
    )
    const { locale } = useI18n()
    
    const percentage = computed(() => {
      if (progress.value === 'indeterminate') return null
    
      if (progress.value < props.min) {
        return 0
      } else if (progress.value > props.max) {
        return 100
      }
    
      return ((progress.value - props.min) / (props.max - props.min)) * 100
    })
    
    const dashOffset = computed(() => 100 - (percentage.value ?? 0))
    </script>
    
    <style lang="postcss" scoped>
    .track {
      stroke: theme('colors.gray.100');
      stroke: color-mix(in oklab, currentColor 15%, transparent 10%);
    }
    
    .value {
      transition: stroke-dashoffset 0.2s;
    }
    
    .circle {
      @apply tw-origin-center;
    
      &:not(.indeterminate) {
        @apply tw-translate-x-0 tw-translate-y-0 tw-skew-x-0 tw-skew-y-0 tw-scale-100 -tw-rotate-90;
      }
    
      &.indeterminate {
        animation: tw-spin 1s linear infinite;
    
        & > .value {
          animation: stretch 3s linear infinite;
        }
      }
    }
    
    @keyframes stretch {
      0%,
      50%,
      100% {
        stroke-dashoffset: 90;
      }
      25%,
      75% {
        stroke-dashoffset: 65;
      }
    }
    </style>