<script setup> import stepperCheck from '@images/svg/stepper-check.svg' const props = defineProps({ items: { type: Array, required: true, }, currentStep: { type: Number, required: false, default: 0, }, direction: { type: String, required: false, default: 'horizontal', }, iconSize: { type: [ String, Number, ], required: false, default: 52, }, isActiveStepValid: { type: Boolean, required: false, default: undefined, }, align: { type: String, required: false, default: 'default', }, }) const emit = defineEmits(['update:currentStep']) const currentStep = ref(props.currentStep || 0) const activeOrCompletedStepsClasses = computed(() => index => index < currentStep.value ? 'stepper-steps-completed' : index === currentStep.value ? 'stepper-steps-active' : '') const isHorizontalAndNotLastStep = computed(() => index => props.direction === 'horizontal' && props.items.length - 1 !== index) // check if validation is enabled const isValidationEnabled = computed(() => { return props.isActiveStepValid !== undefined }) watchEffect(() => { if (props.currentStep !== undefined && props.currentStep < props.items.length && props.currentStep >= 0) currentStep.value = props.currentStep emit('update:currentStep', currentStep.value) }) </script> <template> <VSlideGroup v-model="currentStep" class="app-stepper" show-arrows :direction="props.direction" :class="`app-stepper-${props.align} ${props.items[0].icon ? 'app-stepper-icons' : ''}`" > <VSlideGroupItem v-for="(item, index) in props.items" :key="item.title" :value="index" > <div class="cursor-pointer app-stepper-step mx-1" :class="[ (!props.isActiveStepValid && (isValidationEnabled)) && 'stepper-steps-invalid', activeOrCompletedStepsClasses(index), ]" @click="!isValidationEnabled && emit('update:currentStep', index)" > <!-- SECTION stepper step with icon --> <template v-if="item.icon"> <div class="stepper-icon-step text-high-emphasis d-flex align-center gap-2"> <!-- 👉 icon and title --> <div class="d-flex align-center gap-3 step-wrapper" :class="[props.direction === 'horizontal' && 'flex-column']" > <div class="stepper-icon"> <template v-if="typeof item.icon === 'object'"> <Component :is="item.icon" /> </template> <VIcon v-else :icon="item.icon" :size="item.size || props.iconSize" /> </div> <div> <p class="stepper-title font-weight-medium text-base mb-1"> {{ item.title }} </p> <p v-if="item.subtitle" class="stepper-subtitle text-sm mb-0" > {{ item.subtitle }} </p> </div> </div> <!-- 👉 append chevron --> <VIcon v-if="isHorizontalAndNotLastStep(index)" class="flip-in-rtl stepper-chevron-indicator mx-6" size="18" icon="ri-arrow-right-s-line" /> </div> </template> <!-- !SECTION --> <!-- SECTION stepper step without icon --> <template v-else> <div class="d-flex align-center gap-x-2"> <div class="d-flex align-center gap-2"> <div class="d-flex align-center justify-center" style="block-size: 24px; inline-size: 24px;" > <!-- 👉 custom circle icon --> <template v-if="index >= currentStep"> <div v-if="(!isValidationEnabled || props.isActiveStepValid || index !== currentStep)" class="stepper-step-indicator" /> <VIcon v-else icon="ri-error-warning-line" size="24" color="error" /> </template> <!-- 👉 step completed icon --> <component :is="stepperCheck" v-else class="stepper-step-icon" /> </div> <!-- 👉 Step Number --> <h4 :class="`${!item.subtitle ? 'text-h6' : 'text-h4'} step-number`"> {{ (index + 1).toString().padStart(2, '0') }} </h4> </div> <!-- 👉 title and subtitle --> <div class="app-stepper-title-wrapper" style="line-height: 0;" > <h6 class="text-base font-weight-medium step-title"> {{ item.title }} </h6> <p v-if="item.subtitle" class="text-sm step-subtitle mb-0" > {{ item.subtitle }} </p> </div> <!-- 👉 stepper step line --> <div v-if="isHorizontalAndNotLastStep(index)" class="stepper-step-line" /> </div> <div v-if="props.direction === 'vertical' && props.items.length - 1 !== index" class="stepper-step-line vertical" /> </template> <!-- !SECTION --> </div> </VSlideGroupItem> </VSlideGroup> </template> <style lang="scss"> @use "@core/scss/base/mixins.scss"; /* stylelint-disable no-descending-specificity */ .app-stepper { &.app-stepper-default:not(.app-stepper-icons) .app-stepper-step:not(:last-child) { inline-size: 100%; } // 👉 stepper step with icon and default .v-slide-group__content { .stepper-step-indicator { border: 0.1875rem solid rgb(var(--v-theme-primary)); border-radius: 50%; background-color: rgb(var(--v-theme-surface)); block-size: 1.25rem; inline-size: 1.25rem; opacity: var(--v-activated-opacity); } .stepper-step-line { border-radius: 0.1875rem; background-color: rgb(var(--v-theme-primary)); block-size: 0.1875rem; opacity: var(--v-activated-opacity); } .stepper-step-line:not(.vertical) { inline-size: 100%; min-inline-size: 3rem; } .stepper-step-line.vertical { border-radius: 1.25rem; block-size: 1.25rem; inline-size: 0.1875rem; margin-inline-start: 0.625rem; } .stepper-chevron-indicator { color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)); } .stepper-steps-completed, .stepper-steps-active { .stepper-icon-step, .stepper-step-icon { color: rgb(var(--v-theme-primary)) !important; } .stepper-step-indicator { border-width: 0.3125rem; opacity: 1; } } .stepper-steps-completed { .stepper-step-line { opacity: 1; } .stepper-chevron-indicator { color: rgb(var(--v-theme-primary)); } } .stepper-steps-invalid.stepper-steps-active { .stepper-icon-step, .step-number, .step-title, .step-subtitle { color: rgb(var(--v-theme-error)) !important; } } .app-stepper-step:not(.stepper-steps-active,.stepper-steps-completed) { .step-number { color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)); } } } .app-stepper-title-wrapper { text-wrap: nowrap; } // 👉 stepper step with bg color &.stepper-icon-step-bg { .v-slide-group__content { row-gap: 1.5rem; } .stepper-icon-step { .step-wrapper { flex-direction: row !important; } .stepper-icon { display: flex; align-items: center; justify-content: center; border-radius: 0.3125rem; background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity)); block-size: 2.5rem; color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); inline-size: 2.5rem; margin-inline-end: 0.3rem; } .stepper-title, .stepper-subtitle { line-height: normal; } .stepper-title { color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); font-size: 0.9375rem; line-height: 1.375rem; } .stepper-subtitle { color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); font-size: 0.8125rem; font-weight: 400; line-height: 1.25rem; } } .stepper-steps-active { .stepper-icon-step { .stepper-icon { background-color: rgb(var(--v-theme-primary)); color: rgba(var(--v-theme-on-primary)); @include mixins.elevation(2); } } } .stepper-steps-completed { .stepper-icon-step { .stepper-icon { background: rgba(var(--v-theme-primary), 0.16); color: rgba(var(--v-theme-primary)); } } } } // 👉 stepper alignment &.app-stepper-default { .v-slide-group__content { justify-content: space-between; } } &.app-stepper-center { .v-slide-group__content { justify-content: center; } } &.app-stepper- { .v-slide-group__content { justify-content: start; } } &.app-stepper-end { .v-slide-group__content { justify-content: end; } } &.app-stepper-icons { .app-stepper-step:not(.stepper-steps-active,.stepper-steps-completed) { .stepper-title { color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); } } &:not(.stepper-icon-step-bg) { .step-wrapper { padding-block: 1.25rem; padding-inline: 1.875rem; } } &.v-slide-group--vertical { .step-wrapper { padding-inline: 0; } } } } </style>