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:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user