fix(ui): prevent top bar and header rows from overflowing on mobile

- BreadcrumbBar: add min-w-0/overflow-hidden to container, truncate last breadcrumb item
- IssueDetail: add flex-wrap and min-w-0 to header row, shrink-0 on buttons, truncate project name
- Companies: add flex-wrap and tighter gap on stats row for mobile

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-25 17:40:13 -06:00
parent a701f12059
commit 3717bab673
3 changed files with 38 additions and 18 deletions

View File

@@ -33,9 +33,9 @@ export function BreadcrumbBar() {
// Single breadcrumb = page title (uppercase) // Single breadcrumb = page title (uppercase)
if (breadcrumbs.length === 1) { if (breadcrumbs.length === 1) {
return ( return (
<div className="border-b border-border px-4 md:px-6 h-12 shrink-0 flex items-center"> <div className="border-b border-border px-4 md:px-6 h-12 shrink-0 flex items-center min-w-0 overflow-hidden">
{menuButton} {menuButton}
<h1 className="text-sm font-semibold uppercase tracking-wider"> <h1 className="text-sm font-semibold uppercase tracking-wider truncate">
{breadcrumbs[0].label} {breadcrumbs[0].label}
</h1> </h1>
</div> </div>
@@ -44,18 +44,18 @@ export function BreadcrumbBar() {
// Multiple breadcrumbs = breadcrumb trail // Multiple breadcrumbs = breadcrumb trail
return ( return (
<div className="border-b border-border px-4 md:px-6 h-12 shrink-0 flex items-center"> <div className="border-b border-border px-4 md:px-6 h-12 shrink-0 flex items-center min-w-0 overflow-hidden">
{menuButton} {menuButton}
<Breadcrumb> <Breadcrumb className="min-w-0 overflow-hidden">
<BreadcrumbList> <BreadcrumbList className="flex-nowrap">
{breadcrumbs.map((crumb, i) => { {breadcrumbs.map((crumb, i) => {
const isLast = i === breadcrumbs.length - 1; const isLast = i === breadcrumbs.length - 1;
return ( return (
<Fragment key={i}> <Fragment key={i}>
{i > 0 && <BreadcrumbSeparator />} {i > 0 && <BreadcrumbSeparator />}
<BreadcrumbItem> <BreadcrumbItem className={isLast ? "min-w-0" : "shrink-0"}>
{isLast || !crumb.href ? ( {isLast || !crumb.href ? (
<BreadcrumbPage>{crumb.label}</BreadcrumbPage> <BreadcrumbPage className="truncate">{crumb.label}</BreadcrumbPage>
) : ( ) : (
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link to={crumb.href}>{crumb.label}</Link> <Link to={crumb.href}>{crumb.label}</Link>

View File

@@ -231,7 +231,7 @@ export function Companies() {
</div> </div>
{/* Stats row */} {/* Stats row */}
<div className="flex items-center gap-5 mt-4 text-sm text-muted-foreground"> <div className="flex items-center gap-3 sm:gap-5 mt-4 text-sm text-muted-foreground flex-wrap">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Users className="h-3.5 w-3.5" /> <Users className="h-3.5 w-3.5" />
<span> <span>

View File

@@ -3,6 +3,7 @@ import { useParams, Link, useNavigate } from "react-router-dom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { issuesApi } from "../api/issues"; import { issuesApi } from "../api/issues";
import { activityApi } from "../api/activity"; import { activityApi } from "../api/activity";
import { heartbeatsApi } from "../api/heartbeats";
import { agentsApi } from "../api/agents"; import { agentsApi } from "../api/agents";
import { projectsApi } from "../api/projects"; import { projectsApi } from "../api/projects";
import { useCompany } from "../context/CompanyContext"; import { useCompany } from "../context/CompanyContext";
@@ -186,6 +187,15 @@ export function IssueDetail() {
enabled: !!issueId, enabled: !!issueId,
}); });
const { data: liveRuns } = useQuery({
queryKey: queryKeys.issues.liveRuns(issueId!),
queryFn: () => heartbeatsApi.liveRunsForIssue(issueId!),
enabled: !!issueId && !!selectedCompanyId,
refetchInterval: 3000,
});
const hasLiveRuns = (liveRuns ?? []).length > 0;
const { data: allIssues } = useQuery({ const { data: allIssues } = useQuery({
queryKey: queryKeys.issues.list(selectedCompanyId!), queryKey: queryKeys.issues.list(selectedCompanyId!),
queryFn: () => issuesApi.list(selectedCompanyId!), queryFn: () => issuesApi.list(selectedCompanyId!),
@@ -423,7 +433,7 @@ export function IssueDetail() {
)} )}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 min-w-0 flex-wrap">
<StatusIcon <StatusIcon
status={issue.status} status={issue.status}
onChange={(status) => updateIssue.mutate({ status })} onChange={(status) => updateIssue.mutate({ status })}
@@ -432,15 +442,25 @@ export function IssueDetail() {
priority={issue.priority} priority={issue.priority}
onChange={(priority) => updateIssue.mutate({ priority })} onChange={(priority) => updateIssue.mutate({ priority })}
/> />
<span className="text-sm font-mono text-muted-foreground">{issue.identifier ?? issue.id.slice(0, 8)}</span> <span className="text-sm font-mono text-muted-foreground shrink-0">{issue.identifier ?? issue.id.slice(0, 8)}</span>
{hasLiveRuns && (
<span className="inline-flex items-center gap-1.5 rounded-full bg-cyan-500/10 border border-cyan-500/30 px-2 py-0.5 text-[10px] font-medium text-cyan-400 shrink-0">
<span className="relative flex h-1.5 w-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75" />
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-cyan-400" />
</span>
Live
</span>
)}
{issue.projectId ? ( {issue.projectId ? (
<Link <Link
to={`/projects/${issue.projectId}`} to={`/projects/${issue.projectId}`}
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors rounded px-1 -mx-1 py-0.5" className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors rounded px-1 -mx-1 py-0.5 min-w-0"
> >
<Hexagon className="h-3 w-3 shrink-0" /> <Hexagon className="h-3 w-3 shrink-0" />
{(projects ?? []).find((p) => p.id === issue.projectId)?.name ?? issue.projectId.slice(0, 8)} <span className="truncate">{(projects ?? []).find((p) => p.id === issue.projectId)?.name ?? issue.projectId.slice(0, 8)}</span>
</Link> </Link>
) : ( ) : (
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground opacity-50 px-1 -mx-1 py-0.5"> <span className="inline-flex items-center gap-1 text-xs text-muted-foreground opacity-50 px-1 -mx-1 py-0.5">
@@ -473,7 +493,7 @@ export function IssueDetail() {
<Button <Button
variant="ghost" variant="ghost"
size="icon-xs" size="icon-xs"
className="ml-auto md:hidden" className="ml-auto md:hidden shrink-0"
onClick={() => setMobilePropsOpen(true)} onClick={() => setMobilePropsOpen(true)}
title="Properties" title="Properties"
> >
@@ -482,7 +502,7 @@ export function IssueDetail() {
<Popover open={moreOpen} onOpenChange={setMoreOpen}> <Popover open={moreOpen} onOpenChange={setMoreOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="ghost" size="icon-xs" className="md:ml-auto"> <Button variant="ghost" size="icon-xs" className="md:ml-auto shrink-0">
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className="h-4 w-4" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -599,10 +619,6 @@ export function IssueDetail() {
<Separator /> <Separator />
<LiveRunWidget issueId={issueId!} companyId={selectedCompanyId} />
<Separator />
<Tabs value={detailTab} onValueChange={setDetailTab} className="space-y-3"> <Tabs value={detailTab} onValueChange={setDetailTab} className="space-y-3">
<TabsList variant="line" className="w-full justify-start gap-1"> <TabsList variant="line" className="w-full justify-start gap-1">
<TabsTrigger value="comments" className="gap-1.5"> <TabsTrigger value="comments" className="gap-1.5">
@@ -632,6 +648,10 @@ export function IssueDetail() {
const attachment = await uploadAttachment.mutateAsync(file); const attachment = await uploadAttachment.mutateAsync(file);
return attachment.contentPath; return attachment.contentPath;
}} }}
onAttachImage={async (file) => {
await uploadAttachment.mutateAsync(file);
}}
liveRunSlot={<LiveRunWidget issueId={issueId!} companyId={selectedCompanyId} />}
/> />
</TabsContent> </TabsContent>