Files
ui/src/runtime/components/Progress.vue
2025-05-07 17:23:27 +02:00

190 lines
5.0 KiB
Vue

<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { ProgressRootProps, ProgressRootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/progress'
import type { ComponentConfig } from '../types/utils'
type Progress = ComponentConfig<typeof theme, AppConfig, 'progress'>
export interface ProgressProps extends Pick<ProgressRootProps, 'getValueLabel' | 'modelValue'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
*/
as?: any
/** The maximum progress value. */
max?: number | Array<any>
/** Display the current progress value. */
status?: boolean
/** Whether the progress is visually inverted. */
inverted?: boolean
/**
* @defaultValue 'md'
*/
size?: Progress['variants']['size']
/**
* @defaultValue 'primary'
*/
color?: Progress['variants']['color']
/**
* The orientation of the progress bar.
* @defaultValue 'horizontal'
*/
orientation?: Progress['variants']['orientation']
/**
* The animation of the progress bar.
* @defaultValue 'carousel'
*/
animation?: Progress['variants']['animation']
class?: any
ui?: Progress['slots']
}
export interface ProgressEmits extends ProgressRootEmits {}
export type ProgressSlots = {
status(props: { percent?: number }): any
} & {
[key: string]: (props: { step: number }) => any
}
</script>
<script setup lang="ts">
import { computed } from 'vue'
import { Primitive, ProgressRoot, ProgressIndicator, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import { tv } from '../utils/tv'
const props = withDefaults(defineProps<ProgressProps>(), {
inverted: false,
modelValue: null,
orientation: 'horizontal'
})
const emits = defineEmits<ProgressEmits>()
const slots = defineSlots<ProgressSlots>()
const { dir } = useLocale()
const appConfig = useAppConfig() as Progress['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'getValueLabel', 'modelValue'), emits)
const isIndeterminate = computed(() => rootProps.value.modelValue === null)
const hasSteps = computed(() => Array.isArray(props.max))
const realMax = computed(() => {
if (isIndeterminate.value || !props.max) {
return undefined
}
if (Array.isArray(props.max)) {
return props.max.length - 1
}
return Number(props.max)
})
const percent = computed(() => {
if (isIndeterminate.value) {
return undefined
}
switch (true) {
case rootProps.value.modelValue! < 0: return 0
case rootProps.value.modelValue! > (realMax.value ?? 100): return 100
default: return Math.round((rootProps.value.modelValue! / (realMax.value ?? 100)) * 100)
}
})
const indicatorStyle = computed(() => {
if (percent.value === undefined) {
return
}
if (props.orientation === 'vertical') {
return {
transform: `translateY(${props.inverted ? '' : '-'}${100 - percent.value}%)`
}
} else {
if (dir.value === 'rtl') {
return {
transform: `translateX(${props.inverted ? '-' : ''}${100 - percent.value}%)`
}
} else {
return {
transform: `translateX(${props.inverted ? '' : '-'}${100 - percent.value}%)`
}
}
}
})
const statusStyle = computed(() => {
return {
[props.orientation === 'vertical' ? 'height' : 'width']: percent.value ? `${percent.value}%` : 'fit-content'
}
})
function isActive(index: number) {
return index === Number(props.modelValue)
}
function isFirst(index: number) {
return index === 0
}
function isLast(index: number) {
return index === realMax.value
}
function stepVariant(index: number | string) {
index = Number(index)
if (isActive(index) && !isFirst(index)) {
return 'active'
}
if (isFirst(index) && isActive(index)) {
return 'first'
}
if (isLast(index) && isActive(index)) {
return 'last'
}
return 'other'
}
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.progress || {}) })({
animation: props.animation,
size: props.size,
color: props.color,
orientation: props.orientation,
inverted: props.inverted
}))
</script>
<template>
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
<div v-if="!isIndeterminate && (status || !!slots.status)" :class="ui.status({ class: props.ui?.status })" :style="statusStyle">
<slot name="status" :percent="percent">
{{ percent }}%
</slot>
</div>
<ProgressRoot v-bind="rootProps" :max="realMax" :class="ui.base({ class: props.ui?.base })" style="transform: translateZ(0)">
<ProgressIndicator :class="ui.indicator({ class: props.ui?.indicator })" :style="indicatorStyle" />
</ProgressRoot>
<div v-if="hasSteps" :class="ui.steps({ class: props.ui?.steps })">
<div v-for="(step, index) in max" :key="index" :class="ui.step({ class: props.ui?.step, step: stepVariant(index) })">
<slot :name="`step-${index}`" :step="step">
{{ step }}
</slot>
</div>
</div>
</Primitive>
</template>