diff --git a/packages/ui/package.json b/packages/ui/package.json index eba02dd..d22584f 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -32,7 +32,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "tailwind-merge": "^2.5.3", - "zod": "^3.25.76" + "zod": "^4.3.6" }, "devDependencies": { "@tailwindcss/cli": "^4.1.16", diff --git a/packages/ui/src/components/Progress.tsx b/packages/ui/src/components/Progress.tsx index cb19df1..a1b2a19 100644 --- a/packages/ui/src/components/Progress.tsx +++ b/packages/ui/src/components/Progress.tsx @@ -8,7 +8,7 @@ import { useContext, useState, } from "react"; -import { z } from "zod"; +import { z } from "zod/v4"; interface ProgressContextValue { currentStep: number; @@ -32,6 +32,7 @@ export const stepStateSchema = z.enum([ "completed", "pending", "error", + "disabled", ]); export const stepPropsSchema = z.object({ @@ -39,6 +40,7 @@ export const stepPropsSchema = z.object({ children: z.any().optional(), index: z.number().int().nonnegative().optional(), state: stepStateSchema.optional(), + disabled: z.boolean().optional(), }); export type StepProps = z.infer; @@ -65,13 +67,14 @@ const getStepPropsFromElement = (element: ReactElement): StepProps | null => { return validateStepProps(element.props); }; -const stepContainerVariants = cva("flex items-center space-x-2", { +const stepContainerVariants = cva("flex items-center space-x-2 transition-colors duration-200", { variants: { state: { selected: "text-ctp-blue", completed: "text-ctp-green", pending: "text-ctp-subtext0", error: "text-ctp-red", + disabled: "text-ctp-overlay0 opacity-50", }, }, defaultVariants: { @@ -80,7 +83,7 @@ const stepContainerVariants = cva("flex items-center space-x-2", { }); const stepIndicatorVariants = cva( - "h-8 w-8 rounded-full flex items-center justify-center", + "h-8 w-8 rounded-full flex items-center justify-center transition-colors duration-200", { variants: { state: { @@ -88,6 +91,7 @@ const stepIndicatorVariants = cva( completed: "bg-ctp-green text-ctp-base", pending: "bg-ctp-surface0", error: "bg-ctp-red text-ctp-base", + disabled: "bg-ctp-surface0 opacity-50", }, }, defaultVariants: { @@ -99,12 +103,16 @@ const stepIndicatorVariants = cva( interface StepComponentProps { name: string; children?: ReactNode; + state?: StepState; + disabled?: boolean; } interface StepRenderProps { name: string; index: number; state: StepState; + clickable: boolean; + onClick?: () => void; } const Step = (_props: StepComponentProps) => { @@ -113,25 +121,39 @@ const Step = (_props: StepComponentProps) => { Step.displayName = "Progress.Step"; -const StepRenderer = ({ name, index, state }: StepRenderProps) => { +const StepRenderer = ({ + name, + index, + state, + clickable, + onClick, +}: StepRenderProps) => { const stepNumber = index + 1; + const isClickable = clickable && state !== "disabled"; return ( -
+
+ ); }; const ConnectorLine = ({ isCompleted }: { isCompleted: boolean }) => (
); interface ProgressProps { initialStep?: number; + currentStep?: number; + onStepChange?: (step: number) => void; onNext?: () => void; onBack?: () => void; children: ReactNode; @@ -139,11 +161,16 @@ interface ProgressProps { export const Progress = ({ initialStep = 0, + currentStep: controlledStep, + onStepChange, onNext, onBack, children, }: ProgressProps) => { - const [currentStep, setCurrentStep] = useState(initialStep); + const [internalStep, setInternalStep] = useState(initialStep); + + const isControlled = controlledStep !== undefined; + const currentStep = isControlled ? controlledStep : internalStep; const steps = Children.toArray(children).filter( (child): child is ReactElement => @@ -157,19 +184,23 @@ export const Progress = ({ const canGoNext = currentStep < totalSteps - 1; const canGoBack = currentStep > 0; - const handleNext = () => { - if (!canGoNext) { - return; + const goToStep = (step: number) => { + if (isControlled) { + onStepChange?.(step); + } else { + setInternalStep(step); } - setCurrentStep((prev) => prev + 1); + }; + + const handleNext = () => { + if (!canGoNext) return; + goToStep(currentStep + 1); onNext?.(); }; const handleBack = () => { - if (!canGoBack) { - return; - } - setCurrentStep((prev) => prev - 1); + if (!canGoBack) return; + goToStep(currentStep - 1); onBack?.(); }; @@ -198,13 +229,21 @@ export const Progress = ({ return null; } + const isDisabled = stepProps.disabled === true; const isCurrent = index === currentStep; const isCompleted = index < currentStep; - const stepState: StepState = isCurrent - ? "selected" - : isCompleted - ? "completed" - : "pending"; + + const stepState: StepState = isDisabled + ? "disabled" + : stepProps.state + ? stepProps.state + : isCurrent + ? "selected" + : isCompleted + ? "completed" + : "pending"; + + const clickable = !isDisabled && onStepChange !== undefined; return (
goToStep(index)} /> {index < totalSteps - 1 && ( diff --git a/packages/ui/src/components/SearchableSelect.tsx b/packages/ui/src/components/SearchableSelect.tsx index f0e6b27..e3551e7 100644 --- a/packages/ui/src/components/SearchableSelect.tsx +++ b/packages/ui/src/components/SearchableSelect.tsx @@ -133,7 +133,7 @@ export const SearchableSelect = ({ }; return ( -
+
{label && (