Files
vue-bits/public/r/Stepper.json
David Haz e621971723 jsrepo v3
2025-12-15 23:50:24 +02:00

1 line
9.7 KiB
JSON

{"name":"Stepper","title":"Stepper","description":"Animated multi-step progress indicator with active state transitions.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div class=\"flex justify-center items-center w-full h-full\" v-bind=\"$attrs\">\n <div\n :class=\"`w-full max-w-md p-8 rounded-[2rem] shadow-[0_20px_25px_-5px_rgba(0,0,0,0.1),0_10px_10px_-5px_rgba(0,0,0,0.04)] ${stepCircleContainerClassName}`\"\n style=\"border: 1px solid #222\"\n >\n <div\n :class=\"`flex items-center justify-center w-full ${stepContainerClassName}`\"\n :style=\"{ marginBottom: isCompleted ? '0' : '2rem' }\"\n >\n <template v-for=\"(_, index) in stepsArray\" :key=\"index + 1\">\n <div\n v-if=\"!renderStepIndicator\"\n @click=\"() => handleStepClick(index + 1)\"\n :class=\"[\n 'relative outline-none flex h-8 w-8 items-center justify-center rounded-full font-semibold',\n isCompleted && lockOnComplete ? 'cursor-default' : 'cursor-pointer'\n ]\"\n :style=\"getStepIndicatorStyle(index + 1)\"\n >\n <svg\n v-if=\"getStepStatus(index + 1) === 'complete'\"\n class=\"h-4 w-4 text-black stroke-black\"\n fill=\"none\"\n stroke=\"currentColor\"\n :stroke-width=\"2\"\n viewBox=\"0 0 24 24\"\n >\n <Motion\n as=\"path\"\n d=\"M5 13l4 4L19 7\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n :initial=\"{ pathLength: 0, opacity: 0 }\"\n :animate=\"\n getStepStatus(index + 1) === 'complete'\n ? { pathLength: 1, opacity: 1 }\n : { pathLength: 0, opacity: 0 }\n \"\n />\n </svg>\n <div v-else-if=\"getStepStatus(index + 1) === 'active'\" class=\"h-3 w-3 rounded-full bg-black\" />\n <span v-else class=\"text-sm\">{{ index + 1 }}</span>\n </div>\n\n <component\n v-else\n :is=\"renderStepIndicator\"\n :step=\"index + 1\"\n :current-step=\"currentStep\"\n :on-step-click=\"handleCustomStepClick\"\n />\n\n <div\n v-if=\"index < totalSteps - 1\"\n class=\"relative ml-2 mr-2 h-0.5 flex-1 overflow-hidden rounded bg-zinc-600\"\n >\n <Motion\n as=\"div\"\n class=\"absolute left-0 top-0 h-full\"\n :initial=\"{ width: 0, backgroundColor: '#52525b' }\"\n :animate=\"\n currentStep > index + 1\n ? { width: '100%', backgroundColor: '#27ff64' }\n : { width: 0, backgroundColor: '#52525b' }\n \"\n :transition=\"{ type: 'spring', stiffness: 100, damping: 15, duration: 0.4 }\"\n />\n </div>\n </template>\n </div>\n\n <Motion\n as=\"div\"\n :class=\"`w-full ${contentClassName}`\"\n :style=\"{\n position: 'relative',\n overflow: 'hidden',\n marginBottom: isCompleted ? '0' : '2rem'\n }\"\n :animate=\"{ height: isCompleted ? 0 : `${parentHeight + 1}px` }\"\n :transition=\"{ type: 'spring', stiffness: 200, damping: 25, duration: 0.4 }\"\n >\n <AnimatePresence :initial=\"false\" mode=\"sync\" :custom=\"direction\">\n <Motion\n v-if=\"!isCompleted\"\n ref=\"containerRef\"\n as=\"div\"\n :key=\"currentStep\"\n :initial=\"getStepContentInitial()\"\n :animate=\"{ x: '0%', opacity: 1 }\"\n :exit=\"getStepContentExit()\"\n :transition=\"{ type: 'tween', stiffness: 300, damping: 30, duration: 0.4 }\"\n :style=\"{ position: 'absolute', left: 0, right: 0, top: 0 }\"\n >\n <div ref=\"contentRef\" v-if=\"slots.default && slots.default()[currentStep - 1]\">\n <component :is=\"slots.default()[currentStep - 1]\" />\n </div>\n </Motion>\n </AnimatePresence>\n </Motion>\n\n <div v-if=\"!isCompleted\" :class=\"`w-full ${footerClassName}`\">\n <div :class=\"`flex w-full ${currentStep !== 1 ? 'justify-between' : 'justify-end'}`\">\n <button\n v-if=\"currentStep !== 1\"\n @click=\"handleBack\"\n :disabled=\"backButtonProps?.disabled\"\n :class=\"`text-zinc-400 bg-transparent cursor-pointer transition-all duration-[350ms] rounded px-2 py-1 border-none hover:text-white ${currentStep === 1 ? 'opacity-50 cursor-not-allowed' : ''}`\"\n v-bind=\"backButtonProps\"\n >\n {{ backButtonText }}\n </button>\n <button\n @click=\"isLastStep ? handleComplete() : handleNext()\"\n :disabled=\"nextButtonProps?.disabled\"\n :class=\"`border-none bg-[#27ff64] transition-all duration-[350ms] flex items-center justify-center rounded-full text-black font-medium tracking-tight px-3.5 py-1.5 cursor-pointer hover:bg-[#22e55c] disabled:opacity-50 disabled:cursor-not-allowed`\"\n >\n {{ isLastStep ? 'Complete' : nextButtonText }}\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport {\n ref,\n computed,\n useSlots,\n watch,\n onMounted,\n nextTick,\n useTemplateRef,\n type VNode,\n type ButtonHTMLAttributes,\n type Component\n} from 'vue';\nimport { Motion, AnimatePresence } from 'motion-v';\n\ninterface StepperProps {\n children?: VNode[];\n initialStep?: number;\n onStepChange?: (step: number) => void;\n onFinalStepCompleted?: () => void;\n stepCircleContainerClassName?: string;\n stepContainerClassName?: string;\n contentClassName?: string;\n footerClassName?: string;\n backButtonProps?: ButtonHTMLAttributes;\n nextButtonProps?: ButtonHTMLAttributes;\n backButtonText?: string;\n nextButtonText?: string;\n disableStepIndicators?: boolean;\n renderStepIndicator?: Component;\n lockOnComplete?: boolean;\n}\n\nconst props = withDefaults(defineProps<StepperProps>(), {\n initialStep: 1,\n onStepChange: () => {},\n onFinalStepCompleted: () => {},\n stepCircleContainerClassName: '',\n stepContainerClassName: '',\n contentClassName: '',\n footerClassName: '',\n backButtonProps: () => ({}),\n nextButtonProps: () => ({}),\n backButtonText: 'Back',\n nextButtonText: 'Continue',\n disableStepIndicators: false,\n renderStepIndicator: undefined,\n lockOnComplete: true\n});\n\nconst slots = useSlots();\nconst currentStep = ref(props.initialStep);\nconst direction = ref(1);\nconst isCompleted = ref(false);\nconst parentHeight = ref(0);\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nconst contentRef = useTemplateRef<HTMLDivElement>('contentRef');\n\nconst stepsArray = computed(() => slots.default?.() || []);\nconst totalSteps = computed(() => stepsArray.value.length);\nconst isLastStep = computed(() => currentStep.value === totalSteps.value);\n\nconst getStepStatus = (step: number) => {\n if (isCompleted.value || currentStep.value > step) return 'complete';\n if (currentStep.value === step) return 'active';\n return 'inactive';\n};\n\nconst getStepIndicatorStyle = (step: number) => {\n const status = getStepStatus(step);\n switch (status) {\n case 'active':\n case 'complete':\n return { backgroundColor: '#27FF64', color: '#fff' };\n default:\n return { backgroundColor: '#222', color: '#a3a3a3' };\n }\n};\n\nconst getStepContentInitial = () => ({\n x: direction.value >= 0 ? '-100%' : '100%',\n opacity: 0\n});\n\nconst getStepContentExit = () => ({\n x: direction.value >= 0 ? '50%' : '-50%',\n opacity: 0\n});\n\nconst handleStepClick = (step: number) => {\n if (isCompleted.value && props.lockOnComplete) return;\n if (!props.disableStepIndicators) {\n direction.value = step > currentStep.value ? 1 : -1;\n updateStep(step);\n }\n};\n\nconst handleCustomStepClick = (clicked: number) => {\n if (isCompleted.value && props.lockOnComplete) return;\n if (clicked !== currentStep.value && !props.disableStepIndicators) {\n direction.value = clicked > currentStep.value ? 1 : -1;\n updateStep(clicked);\n }\n};\n\nconst measureHeight = () => {\n nextTick(() => {\n if (contentRef.value) {\n const height = contentRef.value.offsetHeight;\n if (height > 0 && height !== parentHeight.value) {\n parentHeight.value = height;\n }\n }\n });\n};\n\nconst updateStep = (newStep: number) => {\n if (newStep >= 1 && newStep <= totalSteps.value) {\n currentStep.value = newStep;\n }\n};\n\nconst handleBack = () => {\n direction.value = -1;\n updateStep(currentStep.value - 1);\n};\n\nconst handleNext = () => {\n direction.value = 1;\n updateStep(currentStep.value + 1);\n};\n\nconst handleComplete = () => {\n isCompleted.value = true;\n props.onFinalStepCompleted?.();\n};\n\nwatch(currentStep, (newStep, oldStep) => {\n props.onStepChange?.(newStep);\n if (newStep !== oldStep && !isCompleted.value) {\n nextTick(measureHeight);\n } else if (!props.lockOnComplete && isCompleted.value) {\n isCompleted.value = false;\n nextTick(measureHeight);\n }\n});\n\nonMounted(() => {\n if (props.initialStep !== 1) {\n currentStep.value = props.initialStep;\n }\n measureHeight();\n});\n</script>\n","path":"Stepper/Stepper.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.5.0"}],"devDependencies":[],"categories":["Components"]}