Mobile responsiveness fixes for pipeline UI redesign
Build and Push Docker Image / build (push) Successful in 9m31s
Details
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:
parent
59f90ccc37
commit
ae0ac58547
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue