reactjs-multi-stepper 0.0.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-C_M5bBMw.css +1 -0
- package/dist/assets/index-D0qN-L7R.js +8204 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/MultiStepper.tsx +23 -0
- package/src/components/StepperContent.tsx +16 -0
- package/src/components/StepperFooter.tsx +31 -0
- package/src/components/StepperHeader.tsx +35 -0
- package/src/components/StepperIcon.tsx +51 -0
- package/src/components/icon.tsx +28 -0
- package/src/components/spinner.tsx +36 -0
- package/src/contexts/index.tsx +78 -0
- package/src/hooks/index.ts +17 -0
- package/src/index.css +27 -1
- package/src/main.tsx +2 -2
- package/src/types/index.tsx +35 -0
- package/dist/assets/index-B-TwSs7w.js +0 -49
- package/dist/assets/index-CrvTEQ8b.css +0 -1
- package/src/App.tsx +0 -11
package/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Vite + React + TS</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-D0qN-L7R.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C_M5bBMw.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reactjs-multi-stepper",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@tailwindcss/vite": "^4.1.11",
|
|
13
13
|
"@vitejs/plugin-react-swc": "^3.11.0",
|
|
14
|
+
"lucide-react": "^0.535.0",
|
|
14
15
|
"react": "^19.1.0",
|
|
15
16
|
"react-dom": "^19.1.0",
|
|
16
17
|
"tailwindcss": "^4.1.11"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { StepType } from './types';
|
|
3
|
+
import { MultiStepperProvider } from './contexts';
|
|
4
|
+
import { StepperHeader } from './components/StepperHeader';
|
|
5
|
+
import { StepperFooter } from './components/StepperFooter';
|
|
6
|
+
import { StepperContent } from './components/StepperContent';
|
|
7
|
+
|
|
8
|
+
type StepperProps = {
|
|
9
|
+
steps?: StepType[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const MultiStepper: React.FC<StepperProps> = ({
|
|
13
|
+
steps,
|
|
14
|
+
}) => {
|
|
15
|
+
if (!steps) return <></>
|
|
16
|
+
return (
|
|
17
|
+
<MultiStepperProvider steppers={steps}>
|
|
18
|
+
<StepperHeader />
|
|
19
|
+
<StepperContent />
|
|
20
|
+
<StepperFooter />
|
|
21
|
+
</MultiStepperProvider>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useMultiStepper } from '../hooks';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export const StepperContent: React.FC = () => {
|
|
6
|
+
const { steps, currentStep } = useMultiStepper()
|
|
7
|
+
return (
|
|
8
|
+
<div className='w-full p-6 flex justify-center items-center'>
|
|
9
|
+
{
|
|
10
|
+
steps[currentStep] &&
|
|
11
|
+
steps[currentStep].children &&
|
|
12
|
+
steps[currentStep].children
|
|
13
|
+
}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { useMultiStepper } from '../hooks';
|
|
3
|
+
|
|
4
|
+
export const StepperFooter: React.FC = () => {
|
|
5
|
+
const { handleNextStep, handlePrevStep, currentStep, steps } = useMultiStepper();
|
|
6
|
+
const isFinshed = currentStep === steps.length - 1
|
|
7
|
+
|
|
8
|
+
const buttonClass = useMemo(() => ({
|
|
9
|
+
button: ` px-8 py-1.5 text-md rounded-md border border-gray-400`,
|
|
10
|
+
fill: ` border-blue-500 bg-blue-500 text-white`
|
|
11
|
+
}), [])
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className='w-full flex justify-around items-center p-6'>
|
|
15
|
+
{
|
|
16
|
+
currentStep > 0 ?
|
|
17
|
+
<button type="button"
|
|
18
|
+
className={buttonClass.button}
|
|
19
|
+
onClick={handlePrevStep} >Prev</button>
|
|
20
|
+
: <div />
|
|
21
|
+
}
|
|
22
|
+
<button type="button"
|
|
23
|
+
className={`${isFinshed ?
|
|
24
|
+
buttonClass.button + buttonClass.fill :
|
|
25
|
+
buttonClass.button} cursor-pointer`}
|
|
26
|
+
onClick={handleNextStep}>
|
|
27
|
+
{isFinshed ? `Finish` : `Next`}
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React, { Fragment } from 'react';
|
|
2
|
+
import { useMultiStepper } from '../hooks';
|
|
3
|
+
import { Step } from './StepperIcon';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export const StepperHeader: React.FC = () => {
|
|
7
|
+
const { steps } = useMultiStepper()
|
|
8
|
+
|
|
9
|
+
if (!steps.length) return <Fragment />
|
|
10
|
+
return (
|
|
11
|
+
<div className='app-container w-full'>
|
|
12
|
+
<ol className='flex w-full justify-between'>
|
|
13
|
+
{
|
|
14
|
+
steps.map((step, i) => (
|
|
15
|
+
<div
|
|
16
|
+
key={i}
|
|
17
|
+
className={`step-item ${step.active && "active"} ${step.completed && "complete"
|
|
18
|
+
} `}
|
|
19
|
+
>
|
|
20
|
+
|
|
21
|
+
<Step index={i + 1} step={step} />
|
|
22
|
+
|
|
23
|
+
<div className='text-center mt-4'>
|
|
24
|
+
{step.title && <h3 className='text-md text-gray-900 font-medium mb-1'>{step.title}</h3>}
|
|
25
|
+
{step.description && <h3 className='text-sm text-gray-600 font-light'>{step.description}</h3>}
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
</ol>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { Fragment } from 'react';
|
|
2
|
+
import { useMultiStepper } from '../hooks';
|
|
3
|
+
import type { StepType } from '../types';
|
|
4
|
+
import Icon from './icon';
|
|
5
|
+
import Spinner from './spinner';
|
|
6
|
+
|
|
7
|
+
type StepItemType = {
|
|
8
|
+
step: StepType,
|
|
9
|
+
index: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Step: React.FC<StepItemType> = ({
|
|
13
|
+
step, index
|
|
14
|
+
}) => {
|
|
15
|
+
const { steps } = useMultiStepper()
|
|
16
|
+
if (!steps.length) return <Fragment />
|
|
17
|
+
|
|
18
|
+
if (step.loading) return <div className='step step-active'>
|
|
19
|
+
<Spinner />
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
if (step.error) return <div className='step bg-red-500'>
|
|
23
|
+
<Icon name={"X"} className='text-white' />
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
if (step.completed) return <div className='step bg-green-500'>
|
|
27
|
+
<Icon name={"Check"} className='text-white' />
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
if (step.finshed) return <div className='step bg-green-500'>
|
|
31
|
+
<Icon name={"Check"} className='text-white' />
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
if (step.active) return <div className='step bg-blue-500'>
|
|
35
|
+
{
|
|
36
|
+
step.icon ?
|
|
37
|
+
<Icon name={step.icon} className='text-white' />
|
|
38
|
+
: <h2 className='text-white'>{index}</h2>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={`step border border-gray-300`}>
|
|
45
|
+
{
|
|
46
|
+
step.icon ? <Icon name={step.icon} className={`step-icon`} />
|
|
47
|
+
: <h2>{index}</h2>
|
|
48
|
+
}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// components/Icon.tsx
|
|
2
|
+
import * as Icons from "lucide-react";
|
|
3
|
+
import type { LucideProps } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
interface IconProps extends LucideProps {
|
|
6
|
+
name: keyof typeof Icons;
|
|
7
|
+
size?: number;
|
|
8
|
+
color?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Icon = ({
|
|
12
|
+
name,
|
|
13
|
+
size = 20,
|
|
14
|
+
color = "currentColor",
|
|
15
|
+
...props
|
|
16
|
+
}: IconProps) => {
|
|
17
|
+
const LucideIcon = Icons[name];
|
|
18
|
+
if (
|
|
19
|
+
typeof LucideIcon === "function" ||
|
|
20
|
+
(LucideIcon && typeof LucideIcon === "object")
|
|
21
|
+
) {
|
|
22
|
+
// @ts-expect-error: LucideIcon is a valid React component here
|
|
23
|
+
return <LucideIcon size={size} color={color} {...props} />;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default Icon;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import Icon from "./icon";
|
|
3
|
+
// import { cn } from "@/lib/utils"; // or use 'clsx' if you prefer
|
|
4
|
+
|
|
5
|
+
type SpinnerProps = {
|
|
6
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl" | number;
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const sizeMap: Record<string, string> = {
|
|
11
|
+
xs: "w-2 h-2",
|
|
12
|
+
sm: "w-4 h-4",
|
|
13
|
+
md: "w-6 h-6",
|
|
14
|
+
lg: "w-8 h-8",
|
|
15
|
+
xl: "w-10 h-10",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const Spinner: React.FC<SpinnerProps> = ({ size = "md", className }) => {
|
|
19
|
+
const sizeClass = useMemo(() => {
|
|
20
|
+
return typeof size === "number"
|
|
21
|
+
? `w-[${size}px] h-[${size}px]`
|
|
22
|
+
: sizeMap[size] || sizeMap["md"];
|
|
23
|
+
}, [size]);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Icon
|
|
27
|
+
name="Loader2Icon"
|
|
28
|
+
aria-label="Loading"
|
|
29
|
+
role="status"
|
|
30
|
+
// className={cn("animate-spin text-muted", sizeClass, className)}
|
|
31
|
+
className={sizeClass+className}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default React.memo(Spinner);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import type { MultiStepperProviderType, StepType } from '../types';
|
|
3
|
+
import { MultiStepperContext } from "../hooks";
|
|
4
|
+
|
|
5
|
+
export const MultiStepperProvider: React.FC<MultiStepperProviderType> = ({ children, steppers }) => {
|
|
6
|
+
|
|
7
|
+
const [currentStep, setCurrentStep] = useState(0)
|
|
8
|
+
const [steps, setSteps] = useState<StepType[]>([])
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if(steppers.length){
|
|
12
|
+
const temp = [...steppers]
|
|
13
|
+
temp[0].active = true
|
|
14
|
+
setSteps(temp)
|
|
15
|
+
}
|
|
16
|
+
}, [steppers.length])
|
|
17
|
+
|
|
18
|
+
const updateSteps = useCallback((newStep: number) => {
|
|
19
|
+
setSteps((prev) => {
|
|
20
|
+
const updated = [...prev]
|
|
21
|
+
|
|
22
|
+
// if more than length of steps array return steps array as its
|
|
23
|
+
if (newStep > prev.length - 1) return prev
|
|
24
|
+
|
|
25
|
+
// deactivate current step
|
|
26
|
+
if (updated[currentStep])
|
|
27
|
+
updated[currentStep] = { ...updated[currentStep], active: false }
|
|
28
|
+
|
|
29
|
+
// activate new step
|
|
30
|
+
if (updated[newStep])
|
|
31
|
+
updated[newStep] = { ...updated[newStep], active: true }
|
|
32
|
+
|
|
33
|
+
// mark prev steps as completed
|
|
34
|
+
for (let i = 0; i < newStep; i++) {
|
|
35
|
+
updated[i] = { ...updated[i], completed: true }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// mark future steps as incompleted
|
|
39
|
+
for (let i = newStep; i < updated.length; i++) {
|
|
40
|
+
updated[i] = { ...updated[i], completed: false }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return updated
|
|
44
|
+
})
|
|
45
|
+
setCurrentStep(newStep)
|
|
46
|
+
}, [currentStep])
|
|
47
|
+
|
|
48
|
+
const handleNextStep = useCallback(() => {
|
|
49
|
+
if (currentStep < steps.length -1) updateSteps(currentStep + 1)
|
|
50
|
+
else setSteps((prev) => {
|
|
51
|
+
const updated = [...prev]
|
|
52
|
+
updated[currentStep] = { ...updated[currentStep], completed: true }
|
|
53
|
+
return updated
|
|
54
|
+
})
|
|
55
|
+
}, [currentStep, steps.length, updateSteps])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
const handlePrevStep = useCallback(() => {
|
|
60
|
+
if (currentStep > 0) updateSteps(currentStep-1)
|
|
61
|
+
}, [currentStep, updateSteps])
|
|
62
|
+
|
|
63
|
+
// ⚡️ Memoize the entire context value
|
|
64
|
+
const contextValue = useMemo(
|
|
65
|
+
() => ({
|
|
66
|
+
currentStep,
|
|
67
|
+
steps,
|
|
68
|
+
handleNextStep,
|
|
69
|
+
handlePrevStep,
|
|
70
|
+
updateSteps,
|
|
71
|
+
}),
|
|
72
|
+
[currentStep, steps, handleNextStep, handlePrevStep, updateSteps]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return <MultiStepperContext.Provider value={contextValue}>
|
|
76
|
+
{children}
|
|
77
|
+
</MultiStepperContext.Provider>
|
|
78
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { MultiStepperType } from "../types";
|
|
3
|
+
|
|
4
|
+
// Multi stepper context
|
|
5
|
+
export const MultiStepperContext = createContext<MultiStepperType | undefined>(
|
|
6
|
+
undefined
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
// context consumer hook
|
|
10
|
+
export const useMultiStepper = () => {
|
|
11
|
+
const context = useContext(MultiStepperContext);
|
|
12
|
+
if (!context)
|
|
13
|
+
throw new Error(
|
|
14
|
+
"useMultiStepperForm must be used within a MultiStepperProvider"
|
|
15
|
+
);
|
|
16
|
+
return context;
|
|
17
|
+
};
|
package/src/index.css
CHANGED
|
@@ -1 +1,27 @@
|
|
|
1
|
-
@import "tailwindcss";
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
.app-container{
|
|
4
|
+
@apply p-6
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.step-item {
|
|
8
|
+
@apply relative flex flex-col justify-center items-center w-full;
|
|
9
|
+
}
|
|
10
|
+
.step-item:not(:first-child):before {
|
|
11
|
+
@apply content-[''] bg-slate-200 absolute w-[calc(100%-2.5rem)] h-[3px] right-[calc(50%+1.25rem)] top-4.5
|
|
12
|
+
transition-all duration-200;
|
|
13
|
+
}
|
|
14
|
+
.step {
|
|
15
|
+
@apply w-10 h-10 flex items-center justify-center z-10 relative rounded-full font-semibold text-black
|
|
16
|
+
transition-all duration-200;
|
|
17
|
+
}
|
|
18
|
+
.active .step {
|
|
19
|
+
@apply bg-sky-600;
|
|
20
|
+
}
|
|
21
|
+
.complete .step {
|
|
22
|
+
@apply bg-green-600;
|
|
23
|
+
}
|
|
24
|
+
.complete:not(:first-child):before,
|
|
25
|
+
.active:not(:first-child):before {
|
|
26
|
+
@apply bg-green-600;
|
|
27
|
+
}
|
package/src/main.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { StrictMode } from 'react'
|
|
2
2
|
import { createRoot } from 'react-dom/client'
|
|
3
3
|
import './index.css'
|
|
4
|
-
import
|
|
4
|
+
import { MultiStepper } from './MultiStepper.tsx'
|
|
5
5
|
|
|
6
6
|
createRoot(document.getElementById('root')!).render(
|
|
7
7
|
<StrictMode>
|
|
8
|
-
<
|
|
8
|
+
<MultiStepper />
|
|
9
9
|
</StrictMode>,
|
|
10
10
|
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import * as Icons from "lucide-react";
|
|
3
|
+
type StepType = {
|
|
4
|
+
id?: number;
|
|
5
|
+
title?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
icon?: keyof typeof Icons;
|
|
8
|
+
active?: boolean;
|
|
9
|
+
finshed?:boolean;
|
|
10
|
+
error?:boolean;
|
|
11
|
+
loading?:boolean;
|
|
12
|
+
completed?: boolean;
|
|
13
|
+
children?: ReactNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type StepStyleType = {
|
|
17
|
+
activeBgColor?: string;
|
|
18
|
+
activeTextColor?: string;
|
|
19
|
+
completedBgColor?: string;
|
|
20
|
+
completedTextColor?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type MultiStepperType = {
|
|
24
|
+
currentStep: number;
|
|
25
|
+
steps: StepType[];
|
|
26
|
+
handleNextStep: () => void;
|
|
27
|
+
handlePrevStep: () => void;
|
|
28
|
+
updateSteps: (newStep: number) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type MultiStepperProviderType = {
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
steppers: StepType[]
|
|
34
|
+
}
|
|
35
|
+
export type { StepStyleType, StepType, MultiStepperType, MultiStepperProviderType }
|