Mobile responsiveness fixes for pipeline UI redesign
Build and Push Docker Image / build (push) Successful in 9m31s Details

- Detail page header: stack on mobile, icon-only Advanced button on small screens
- InlineEditableText: show pencil icon on mobile (not hover-only)
- EditableCard: show Edit button on mobile (not hover-only)
- PipelineFlowchart: add right-edge fade gradient as scroll hint on mobile
- Summary cards: always 3-col grid (compact on mobile)
- Track switcher: add overflow-x-auto for horizontal scroll

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-02-14 01:59:42 +01:00
parent 59f90ccc37
commit ae0ac58547
4 changed files with 124 additions and 113 deletions

View File

@ -199,127 +199,132 @@ export default function PipelineDetailPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="space-y-3">
<div className="flex items-center gap-3"> <div className="flex items-start justify-between gap-2">
<Link href="/admin/rounds/pipelines"> <div className="flex items-start gap-3 min-w-0">
<Button variant="ghost" size="icon"> <Link href="/admin/rounds/pipelines" className="mt-1">
<ArrowLeft className="h-4 w-4" /> <Button variant="ghost" size="icon" className="h-8 w-8 shrink-0">
</Button> <ArrowLeft className="h-4 w-4" />
</Link> </Button>
<div> </Link>
<div className="flex items-center gap-2"> <div className="min-w-0">
<InlineEditableText <div className="flex flex-wrap items-center gap-2">
value={pipeline.name} <InlineEditableText
onSave={(newName) => updatePipeline({ name: newName })} value={pipeline.name}
variant="h1" onSave={(newName) => updatePipeline({ name: newName })}
placeholder="Untitled Pipeline" variant="h1"
disabled={isUpdating} placeholder="Untitled Pipeline"
/> disabled={isUpdating}
<DropdownMenu> />
<DropdownMenuTrigger asChild> <DropdownMenu>
<button <DropdownMenuTrigger asChild>
className={cn( <button
'inline-flex items-center gap-1 text-[10px] px-2 py-1 rounded-full transition-colors', className={cn(
statusColors[pipeline.status] ?? '', 'inline-flex items-center gap-1 text-[10px] px-2 py-1 rounded-full transition-colors shrink-0',
'hover:opacity-80' statusColors[pipeline.status] ?? '',
)} 'hover:opacity-80'
> )}
{pipeline.status} >
<ChevronDown className="h-3 w-3" /> {pipeline.status}
</button> <ChevronDown className="h-3 w-3" />
</DropdownMenuTrigger> </button>
<DropdownMenuContent align="start"> </DropdownMenuTrigger>
<DropdownMenuItem <DropdownMenuContent align="start">
onClick={() => handleStatusChange('DRAFT')} <DropdownMenuItem
disabled={pipeline.status === 'DRAFT' || updateMutation.isPending} onClick={() => handleStatusChange('DRAFT')}
> disabled={pipeline.status === 'DRAFT' || updateMutation.isPending}
Draft >
</DropdownMenuItem> Draft
<DropdownMenuItem </DropdownMenuItem>
onClick={() => handleStatusChange('ACTIVE')} <DropdownMenuItem
disabled={pipeline.status === 'ACTIVE' || updateMutation.isPending} onClick={() => handleStatusChange('ACTIVE')}
> disabled={pipeline.status === 'ACTIVE' || updateMutation.isPending}
Active >
</DropdownMenuItem> Active
<DropdownMenuItem </DropdownMenuItem>
onClick={() => handleStatusChange('CLOSED')} <DropdownMenuItem
disabled={pipeline.status === 'CLOSED' || updateMutation.isPending} onClick={() => handleStatusChange('CLOSED')}
> disabled={pipeline.status === 'CLOSED' || updateMutation.isPending}
Closed >
</DropdownMenuItem> Closed
<DropdownMenuSeparator /> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuSeparator />
onClick={() => handleStatusChange('ARCHIVED')} <DropdownMenuItem
disabled={pipeline.status === 'ARCHIVED' || updateMutation.isPending} onClick={() => handleStatusChange('ARCHIVED')}
> disabled={pipeline.status === 'ARCHIVED' || updateMutation.isPending}
Archived >
</DropdownMenuItem> Archived
</DropdownMenuContent> </DropdownMenuItem>
</DropdownMenu> </DropdownMenuContent>
</div> </DropdownMenu>
<div className="flex items-center gap-1 text-sm"> </div>
<span className="text-muted-foreground">slug:</span> <div className="flex items-center gap-1 text-sm">
<InlineEditableText <span className="text-muted-foreground">slug:</span>
value={pipeline.slug} <InlineEditableText
onSave={(newSlug) => updatePipeline({ slug: newSlug })} value={pipeline.slug}
variant="mono" onSave={(newSlug) => updatePipeline({ slug: newSlug })}
placeholder="pipeline-slug" variant="mono"
disabled={isUpdating} placeholder="pipeline-slug"
/> disabled={isUpdating}
/>
</div>
</div> </div>
</div> </div>
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-1 sm:gap-2 shrink-0">
<Link href={`/admin/rounds/pipeline/${pipelineId}/advanced` as Route}> <Link href={`/admin/rounds/pipeline/${pipelineId}/advanced` as Route}>
<Button variant="outline" size="sm"> <Button variant="outline" size="icon" className="h-8 w-8 sm:hidden">
<Settings2 className="h-4 w-4 mr-1" /> <Settings2 className="h-4 w-4" />
Advanced
</Button>
</Link>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
</Button> </Button>
</DropdownMenuTrigger> <Button variant="outline" size="sm" className="hidden sm:inline-flex">
<DropdownMenuContent align="end"> <Settings2 className="h-4 w-4 mr-1" />
{pipeline.status === 'DRAFT' && ( Advanced
<DropdownMenuItem </Button>
disabled={publishMutation.isPending} </Link>
onClick={() => publishMutation.mutate({ id: pipelineId })} <DropdownMenu>
> <DropdownMenuTrigger asChild>
{publishMutation.isPending ? ( <Button variant="outline" size="icon" className="h-8 w-8">
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> <MoreHorizontal className="h-4 w-4" />
) : ( </Button>
<Rocket className="h-4 w-4 mr-2" /> </DropdownMenuTrigger>
)} <DropdownMenuContent align="end">
Publish {pipeline.status === 'DRAFT' && (
</DropdownMenuItem> <DropdownMenuItem
)} disabled={publishMutation.isPending}
{pipeline.status === 'ACTIVE' && ( onClick={() => publishMutation.mutate({ id: pipelineId })}
>
{publishMutation.isPending ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Rocket className="h-4 w-4 mr-2" />
)}
Publish
</DropdownMenuItem>
)}
{pipeline.status === 'ACTIVE' && (
<DropdownMenuItem
disabled={updateMutation.isPending}
onClick={() => handleStatusChange('CLOSED')}
>
Close Pipeline
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
disabled={updateMutation.isPending} disabled={updateMutation.isPending}
onClick={() => handleStatusChange('CLOSED')} onClick={() => handleStatusChange('ARCHIVED')}
> >
Close Pipeline <Archive className="h-4 w-4 mr-2" />
Archive
</DropdownMenuItem> </DropdownMenuItem>
)} </DropdownMenuContent>
<DropdownMenuSeparator /> </DropdownMenu>
<DropdownMenuItem </div>
disabled={updateMutation.isPending}
onClick={() => handleStatusChange('ARCHIVED')}
>
<Archive className="h-4 w-4 mr-2" />
Archive
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
{/* Pipeline Summary */} {/* Pipeline Summary */}
<div className="grid gap-4 sm:grid-cols-3"> <div className="grid gap-3 grid-cols-3">
<Card> <Card>
<CardContent className="pt-4"> <CardContent className="pt-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -369,7 +374,7 @@ export default function PipelineDetailPage() {
{/* Track Switcher (only if multiple tracks) */} {/* Track Switcher (only if multiple tracks) */}
{pipeline.tracks.length > 1 && ( {pipeline.tracks.length > 1 && (
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap overflow-x-auto pb-1">
{pipeline.tracks {pipeline.tracks
.sort((a, b) => a.sortOrder - b.sortOrder) .sort((a, b) => a.sortOrder - b.sortOrder)
.map((track) => ( .map((track) => (

View File

@ -79,8 +79,9 @@ export function PipelineFlowchart({
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className={cn('overflow-x-auto rounded-lg border bg-card', className)} className={cn('relative rounded-lg border bg-card', className)}
> >
<div className="overflow-x-auto">
<svg <svg
width={totalWidth} width={totalWidth}
height={totalHeight} height={totalHeight}
@ -265,6 +266,11 @@ export function PipelineFlowchart({
) )
})} })}
</svg> </svg>
</div>
{/* Scroll hint gradient for mobile */}
{totalWidth > 400 && (
<div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-card to-transparent pointer-events-none sm:hidden" />
)}
</div> </div>
) )
} }

View File

@ -56,7 +56,7 @@ export function EditableCard({
onClick={() => setIsEditing(true)} onClick={() => setIsEditing(true)}
className={cn( className={cn(
'h-7 gap-1.5 text-xs', 'h-7 gap-1.5 text-xs',
!alwaysShowEdit && 'opacity-0 group-hover:opacity-100 transition-opacity' !alwaysShowEdit && 'sm:opacity-0 sm:group-hover:opacity-100 transition-opacity'
)} )}
> >
<Pencil className="h-3 w-3" /> <Pencil className="h-3 w-3" />

View File

@ -171,7 +171,7 @@ export function InlineEditableText({
<span className={cn(!value && 'text-muted-foreground italic')}> <span className={cn(!value && 'text-muted-foreground italic')}>
{value || placeholder} {value || placeholder}
</span> </span>
<Pencil className="h-3 w-3 shrink-0 opacity-0 group-hover:opacity-50 transition-opacity" /> <Pencil className="h-3 w-3 shrink-0 opacity-30 sm:opacity-0 sm:group-hover:opacity-50 transition-opacity" />
</motion.button> </motion.button>
)} )}
</AnimatePresence> </AnimatePresence>