Pipeline UI/UX redesign: inline editing, flowchart, sidebar stepper
- Add InlineEditableText, EditableCard, SidebarStepper shared components - Add PipelineFlowchart (interactive SVG stage visualization) - Add StageConfigEditor and usePipelineInlineEdit hook - Redesign detail page: flowchart replaces nested tabs, inline editing - Redesign creation wizard: sidebar stepper replaces accordion sections - Enhance list page: status dots, track indicators, relative timestamps - Convert edit page to redirect (editing now inline on detail page) - Delete old WizardSection accordion component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
262
src/components/ui/sidebar-stepper.tsx
Normal file
262
src/components/ui/sidebar-stepper.tsx
Normal file
@@ -0,0 +1,262 @@
|
||||
'use client'
|
||||
|
||||
import { motion, AnimatePresence } from 'motion/react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { CheckCircle2, AlertCircle, Loader2, Save, Rocket } from 'lucide-react'
|
||||
|
||||
export type StepConfig = {
|
||||
title: string
|
||||
description?: string
|
||||
isValid?: boolean
|
||||
hasErrors?: boolean
|
||||
}
|
||||
|
||||
type SidebarStepperProps = {
|
||||
steps: StepConfig[]
|
||||
currentStep: number
|
||||
onStepChange: (index: number) => void
|
||||
onSave?: () => void
|
||||
onSubmit?: () => void
|
||||
isSaving?: boolean
|
||||
isSubmitting?: boolean
|
||||
saveLabel?: string
|
||||
submitLabel?: string
|
||||
canSubmit?: boolean
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function SidebarStepper({
|
||||
steps,
|
||||
currentStep,
|
||||
onStepChange,
|
||||
onSave,
|
||||
onSubmit,
|
||||
isSaving = false,
|
||||
isSubmitting = false,
|
||||
saveLabel = 'Save Draft',
|
||||
submitLabel = 'Create Pipeline',
|
||||
canSubmit = true,
|
||||
children,
|
||||
className,
|
||||
}: SidebarStepperProps) {
|
||||
const direction = (prev: number, next: number) => (next > prev ? 1 : -1)
|
||||
|
||||
return (
|
||||
<div className={cn('flex gap-6 min-h-[600px]', className)}>
|
||||
{/* Sidebar - hidden on mobile */}
|
||||
<div className="hidden lg:flex lg:flex-col lg:w-[260px] lg:shrink-0">
|
||||
<nav className="flex-1 space-y-1 py-2">
|
||||
{steps.map((step, index) => {
|
||||
const isCurrent = index === currentStep
|
||||
const isComplete = step.isValid === true
|
||||
const hasErrors = step.hasErrors === true
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
onClick={() => onStepChange(index)}
|
||||
className={cn(
|
||||
'w-full flex items-start gap-3 rounded-lg px-3 py-2.5 text-left transition-colors',
|
||||
isCurrent
|
||||
? 'bg-primary/5 border border-primary/20'
|
||||
: 'hover:bg-muted/50 border border-transparent'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-bold transition-colors',
|
||||
isCurrent && 'bg-primary text-primary-foreground',
|
||||
!isCurrent && isComplete && 'bg-emerald-500 text-white',
|
||||
!isCurrent && hasErrors && 'bg-destructive/10 text-destructive border border-destructive/30',
|
||||
!isCurrent && !isComplete && !hasErrors && 'bg-muted text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{isComplete && !isCurrent ? (
|
||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||
) : hasErrors && !isCurrent ? (
|
||||
<AlertCircle className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
index + 1
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p
|
||||
className={cn(
|
||||
'text-sm font-medium truncate',
|
||||
isCurrent && 'text-primary',
|
||||
!isCurrent && 'text-foreground'
|
||||
)}
|
||||
>
|
||||
{step.title}
|
||||
</p>
|
||||
{step.description && (
|
||||
<p className="text-[11px] text-muted-foreground truncate mt-0.5">
|
||||
{step.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="border-t pt-4 space-y-2 mt-auto">
|
||||
{onSave && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
disabled={isSaving || isSubmitting}
|
||||
onClick={onSave}
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{saveLabel}
|
||||
</Button>
|
||||
)}
|
||||
{onSubmit && (
|
||||
<Button
|
||||
className="w-full justify-start"
|
||||
disabled={isSubmitting || isSaving || !canSubmit}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Rocket className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{submitLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile step indicator */}
|
||||
<div className="lg:hidden flex flex-col w-full">
|
||||
<MobileStepIndicator
|
||||
steps={steps}
|
||||
currentStep={currentStep}
|
||||
onStepChange={onStepChange}
|
||||
/>
|
||||
<div className="flex-1 mt-4">
|
||||
<StepContent currentStep={currentStep} direction={direction}>
|
||||
{children}
|
||||
</StepContent>
|
||||
</div>
|
||||
{/* Mobile actions */}
|
||||
<div className="flex gap-2 pt-4 border-t mt-4">
|
||||
{onSave && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
disabled={isSaving || isSubmitting}
|
||||
onClick={onSave}
|
||||
>
|
||||
{isSaving ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <Save className="h-4 w-4 mr-1" />}
|
||||
{saveLabel}
|
||||
</Button>
|
||||
)}
|
||||
{onSubmit && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
disabled={isSubmitting || isSaving || !canSubmit}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{isSubmitting ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <Rocket className="h-4 w-4 mr-1" />}
|
||||
{submitLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop content */}
|
||||
<div className="hidden lg:block flex-1 min-w-0">
|
||||
<StepContent currentStep={currentStep} direction={direction}>
|
||||
{children}
|
||||
</StepContent>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileStepIndicator({
|
||||
steps,
|
||||
currentStep,
|
||||
onStepChange,
|
||||
}: {
|
||||
steps: StepConfig[]
|
||||
currentStep: number
|
||||
onStepChange: (index: number) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="flex items-center gap-1 pb-2 min-w-max">
|
||||
{steps.map((step, index) => {
|
||||
const isCurrent = index === currentStep
|
||||
const isComplete = step.isValid === true
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
onClick={() => onStepChange(index)}
|
||||
className={cn(
|
||||
'flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium transition-colors shrink-0',
|
||||
isCurrent && 'bg-primary text-primary-foreground',
|
||||
!isCurrent && isComplete && 'bg-emerald-100 text-emerald-700',
|
||||
!isCurrent && !isComplete && 'bg-muted text-muted-foreground hover:bg-muted/80'
|
||||
)}
|
||||
>
|
||||
{isComplete && !isCurrent ? (
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
) : (
|
||||
<span>{index + 1}</span>
|
||||
)}
|
||||
<span className="hidden sm:inline">{step.title}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StepContent({
|
||||
currentStep,
|
||||
direction,
|
||||
children,
|
||||
}: {
|
||||
currentStep: number
|
||||
direction: (prev: number, next: number) => number
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const childArray = Array.isArray(children) ? children : [children]
|
||||
const currentChild = childArray[currentStep]
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden">
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<motion.div
|
||||
key={currentStep}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
>
|
||||
{currentChild}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user