@@ -72,14 +78,15 @@ export const AddTitleFormPartial = ({
void onFormSubmit()}
/>
diff --git a/packages/ui/primitives/document-flow/document-flow-root.tsx b/packages/ui/primitives/document-flow/document-flow-root.tsx
index aec74dd6c..42b70c58a 100644
--- a/packages/ui/primitives/document-flow/document-flow-root.tsx
+++ b/packages/ui/primitives/document-flow/document-flow-root.tsx
@@ -1,11 +1,12 @@
'use client';
-import React, { HTMLAttributes } from 'react';
+import type { HTMLAttributes } from 'react';
+import React from 'react';
import { motion } from 'framer-motion';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
+import { cn } from '../../lib/utils';
+import { Button } from '../button';
export type DocumentFlowFormContainerProps = HTMLAttributes & {
children?: React.ReactNode;
diff --git a/packages/ui/primitives/document-flow/field-item.tsx b/packages/ui/primitives/document-flow/field-item.tsx
index 48e52b9a7..7583bd4b9 100644
--- a/packages/ui/primitives/document-flow/field-item.tsx
+++ b/packages/ui/primitives/document-flow/field-item.tsx
@@ -7,10 +7,11 @@ import { createPortal } from 'react-dom';
import { Rnd } from 'react-rnd';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
-import { cn } from '@documenso/ui/lib/utils';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import { FRIENDLY_FIELD_TYPE, TDocumentFlowFormSchema } from './types';
+import { cn } from '../../lib/utils';
+import { Card, CardContent } from '../card';
+import type { TDocumentFlowFormSchema } from './types';
+import { FRIENDLY_FIELD_TYPE } from './types';
type Field = TDocumentFlowFormSchema['fields'][0];
diff --git a/packages/ui/primitives/document-flow/send-document-action-dialog.tsx b/packages/ui/primitives/document-flow/send-document-action-dialog.tsx
index f295dadfc..a70282800 100644
--- a/packages/ui/primitives/document-flow/send-document-action-dialog.tsx
+++ b/packages/ui/primitives/document-flow/send-document-action-dialog.tsx
@@ -2,7 +2,8 @@ import { useState } from 'react';
import { Loader } from 'lucide-react';
-import { Button, ButtonProps } from '@documenso/ui/primitives/button';
+import type { ButtonProps } from '../button';
+import { Button } from '../button';
import {
Dialog,
DialogContent,
@@ -11,7 +12,7 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
+} from '../dialog';
export type SendDocumentActionDialogProps = ButtonProps & {
loading?: boolean;
diff --git a/packages/ui/primitives/document-flow/single-player-mode-fields.tsx b/packages/ui/primitives/document-flow/single-player-mode-fields.tsx
index 04c093efc..7cecd7131 100644
--- a/packages/ui/primitives/document-flow/single-player-mode-fields.tsx
+++ b/packages/ui/primitives/document-flow/single-player-mode-fields.tsx
@@ -13,9 +13,11 @@ import {
MIN_HANDWRITING_FONT_SIZE,
MIN_STANDARD_FONT_SIZE,
} from '@documenso/lib/constants/pdf';
-import { Field, FieldType } from '@documenso/prisma/client';
-import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
-import { FieldRootContainer } from '@documenso/ui/components/field/field';
+import type { Field } from '@documenso/prisma/client';
+import { FieldType } from '@documenso/prisma/client';
+import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
+
+import { FieldRootContainer } from '../../components/field/field';
export type SinglePlayerModeFieldContainerProps = {
field: FieldWithSignature;
diff --git a/packages/ui/primitives/document-flow/types.ts b/packages/ui/primitives/document-flow/types.ts
index c9244ad05..677dc931b 100644
--- a/packages/ui/primitives/document-flow/types.ts
+++ b/packages/ui/primitives/document-flow/types.ts
@@ -53,7 +53,7 @@ export const FRIENDLY_FIELD_TYPE: Record = {
export interface DocumentFlowStep {
title: string;
description: string;
- stepIndex: number;
+ stepIndex?: number;
onBackStep?: () => unknown;
onNextStep?: () => unknown;
}
diff --git a/packages/ui/primitives/form/form-error-message.tsx b/packages/ui/primitives/form/form-error-message.tsx
index bb555b7f7..e429799da 100644
--- a/packages/ui/primitives/form/form-error-message.tsx
+++ b/packages/ui/primitives/form/form-error-message.tsx
@@ -1,6 +1,6 @@
import { AnimatePresence, motion } from 'framer-motion';
-import { cn } from '@documenso/ui/lib/utils';
+import { cn } from '../../lib/utils';
export type FormErrorMessageProps = {
className?: string;
diff --git a/packages/ui/primitives/form/form.tsx b/packages/ui/primitives/form/form.tsx
index 9467de3af..f500accae 100644
--- a/packages/ui/primitives/form/form.tsx
+++ b/packages/ui/primitives/form/form.tsx
@@ -1,19 +1,12 @@
import * as React from 'react';
-import * as LabelPrimitive from '@radix-ui/react-label';
+import type * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import { AnimatePresence, motion } from 'framer-motion';
-import {
- Controller,
- ControllerProps,
- FieldPath,
- FieldValues,
- FormProvider,
- useFormContext,
-} from 'react-hook-form';
-
-import { cn } from '@documenso/ui/lib/utils';
+import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
+import { Controller, FormProvider, useFormContext } from 'react-hook-form';
+import { cn } from '../../lib/utils';
import { Label } from '../label';
const Form = FormProvider;
diff --git a/packages/ui/primitives/pdf-viewer.tsx b/packages/ui/primitives/pdf-viewer.tsx
index 62b08d2f9..07cdaf1e2 100644
--- a/packages/ui/primitives/pdf-viewer.tsx
+++ b/packages/ui/primitives/pdf-viewer.tsx
@@ -3,16 +3,16 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Loader } from 'lucide-react';
-import { PDFDocumentProxy } from 'pdfjs-dist';
+import type { PDFDocumentProxy } from 'pdfjs-dist';
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getFile } from '@documenso/lib/universal/upload/get-file';
-import { DocumentData } from '@documenso/prisma/client';
-import { cn } from '@documenso/ui/lib/utils';
+import type { DocumentData } from '@documenso/prisma/client';
+import { cn } from '../lib/utils';
import { useToast } from './use-toast';
export type LoadedPDFDocument = PDFDocumentProxy;
diff --git a/packages/ui/primitives/signature-pad/signature-pad.tsx b/packages/ui/primitives/signature-pad/signature-pad.tsx
index 107627240..3497418d7 100644
--- a/packages/ui/primitives/signature-pad/signature-pad.tsx
+++ b/packages/ui/primitives/signature-pad/signature-pad.tsx
@@ -1,20 +1,12 @@
'use client';
-import {
- HTMLAttributes,
- MouseEvent,
- PointerEvent,
- TouchEvent,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from 'react';
+import type { HTMLAttributes, MouseEvent, PointerEvent, TouchEvent } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
-import { StrokeOptions, getStroke } from 'perfect-freehand';
-
-import { cn } from '@documenso/ui/lib/utils';
+import type { StrokeOptions } from 'perfect-freehand';
+import { getStroke } from 'perfect-freehand';
+import { cn } from '../../lib/utils';
import { getSvgPathFromStroke } from './helper';
import { Point } from './point';
diff --git a/packages/ui/primitives/stepper.tsx b/packages/ui/primitives/stepper.tsx
new file mode 100644
index 000000000..d38de2eb1
--- /dev/null
+++ b/packages/ui/primitives/stepper.tsx
@@ -0,0 +1,109 @@
+import React, { createContext, useContext, useState } from 'react';
+import type { FC } from 'react';
+
+type StepContextValue = {
+ isCompleting: boolean;
+ stepIndex: number;
+ currentStep: number;
+ totalSteps: number;
+ isFirst: boolean;
+ isLast: boolean;
+ nextStep: () => void;
+ previousStep: () => void;
+};
+
+const StepContext = createContext(null);
+
+type StepperProps = {
+ children: React.ReactNode;
+ onComplete?: () => void | Promise;
+ onStepChanged?: (currentStep: number) => void;
+ currentStep?: number; // external control prop
+ setCurrentStep?: (step: number) => void; // external control function
+};
+
+export const Stepper: FC = ({
+ children,
+ onComplete,
+ onStepChanged,
+ currentStep: propCurrentStep,
+ setCurrentStep: propSetCurrentStep,
+}) => {
+ const [stateCurrentStep, stateSetCurrentStep] = useState(1);
+ const [isCompleting, setIsCompleting] = useState(false);
+
+ // Determine if props are provided, otherwise use state
+ const isControlled = propCurrentStep !== undefined && propSetCurrentStep !== undefined;
+ const currentStep = isControlled ? propCurrentStep : stateCurrentStep;
+ const setCurrentStep = isControlled ? propSetCurrentStep : stateSetCurrentStep;
+
+ const totalSteps = React.Children.count(children);
+
+ const handleComplete = async () => {
+ try {
+ if (!onComplete) {
+ return;
+ }
+
+ setIsCompleting(true);
+
+ await onComplete();
+
+ setIsCompleting(false);
+ } catch (error) {
+ setIsCompleting(false);
+
+ throw error;
+ }
+ };
+
+ const handleStepChange = (nextStep: number) => {
+ setCurrentStep(nextStep);
+ onStepChanged?.(nextStep);
+ };
+
+ const nextStep = () => {
+ if (currentStep < totalSteps) {
+ void handleStepChange(currentStep + 1);
+ } else {
+ void handleComplete();
+ }
+ };
+
+ const previousStep = () => {
+ if (currentStep > 1) {
+ void handleStepChange(currentStep - 1);
+ }
+ };
+
+ // Empty stepper
+ if (totalSteps === 0) {
+ return null;
+ }
+
+ const currentChild = React.Children.toArray(children)[currentStep - 1];
+
+ const stepContextValue: StepContextValue = {
+ isCompleting,
+ stepIndex: currentStep - 1,
+ currentStep,
+ totalSteps,
+ isFirst: currentStep === 1,
+ isLast: currentStep === totalSteps,
+ nextStep,
+ previousStep,
+ };
+
+ return {currentChild};
+};
+
+/** Hook for children to use the step context */
+export const useStep = (): StepContextValue => {
+ const context = useContext(StepContext);
+
+ if (!context) {
+ throw new Error('useStep must be used within a Stepper');
+ }
+
+ return context;
+};