fix(ui): mobile viewport, scrollable popovers, and actor labels
- Set viewport-fit=cover and disable user scaling for mobile PWA feel - Wrap assignee/project popover lists in scrollable containers - Remove rounded-t-sm from stacked chart bars for cleaner rendering - Prevent filter bar icons from shrinking on narrow screens - Show "Board" instead of raw user IDs in activity feeds and toasts - Surface server error message in health API failures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -101,12 +101,13 @@ export function ActivityRow({ event, agentMap, entityNameMap, className }: Activ
|
||||
: entityLink(event.entityType, event.entityId, name);
|
||||
|
||||
const actor = event.actorType === "agent" ? agentMap.get(event.actorId) : null;
|
||||
const actorName = actor?.name ?? (event.actorType === "system" ? "System" : event.actorType === "user" ? "Board" : event.actorId || "Unknown");
|
||||
|
||||
const inner = (
|
||||
<div className="flex gap-3">
|
||||
<p className="flex-1 min-w-0">
|
||||
<Identity
|
||||
name={actor?.name ?? (event.actorType === "system" ? "System" : event.actorId || "You")}
|
||||
name={actorName}
|
||||
size="xs"
|
||||
className="align-baseline"
|
||||
/>
|
||||
|
||||
@@ -106,35 +106,37 @@ export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) {
|
||||
onChange={(e) => setAssigneeSearch(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
!issue.assigneeAgentId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { onUpdate({ assigneeAgentId: null }); setAssigneeOpen(false); }}
|
||||
>
|
||||
No assignee
|
||||
</button>
|
||||
{(agents ?? [])
|
||||
.filter((a) => a.status !== "terminated")
|
||||
.filter((a) => {
|
||||
if (!assigneeSearch.trim()) return true;
|
||||
const q = assigneeSearch.toLowerCase();
|
||||
return a.name.toLowerCase().includes(q);
|
||||
})
|
||||
.map((a) => (
|
||||
<div className="max-h-48 overflow-y-auto overscroll-contain">
|
||||
<button
|
||||
key={a.id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
a.id === issue.assigneeAgentId && "bg-accent"
|
||||
!issue.assigneeAgentId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { onUpdate({ assigneeAgentId: a.id }); setAssigneeOpen(false); }}
|
||||
onClick={() => { onUpdate({ assigneeAgentId: null }); setAssigneeOpen(false); }}
|
||||
>
|
||||
<AgentIcon icon={a.icon} className="shrink-0 h-3 w-3 text-muted-foreground" />
|
||||
{a.name}
|
||||
No assignee
|
||||
</button>
|
||||
))}
|
||||
{(agents ?? [])
|
||||
.filter((a) => a.status !== "terminated")
|
||||
.filter((a) => {
|
||||
if (!assigneeSearch.trim()) return true;
|
||||
const q = assigneeSearch.toLowerCase();
|
||||
return a.name.toLowerCase().includes(q);
|
||||
})
|
||||
.map((a) => (
|
||||
<button
|
||||
key={a.id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
a.id === issue.assigneeAgentId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { onUpdate({ assigneeAgentId: a.id }); setAssigneeOpen(false); }}
|
||||
>
|
||||
<AgentIcon icon={a.icon} className="shrink-0 h-3 w-3 text-muted-foreground" />
|
||||
{a.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{issue.assigneeAgentId && (
|
||||
@@ -176,37 +178,39 @@ export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) {
|
||||
onChange={(e) => setProjectSearch(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 whitespace-nowrap",
|
||||
!issue.projectId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { onUpdate({ projectId: null }); setProjectOpen(false); }}
|
||||
>
|
||||
No project
|
||||
</button>
|
||||
{(projects ?? [])
|
||||
.filter((p) => {
|
||||
if (!projectSearch.trim()) return true;
|
||||
const q = projectSearch.toLowerCase();
|
||||
return p.name.toLowerCase().includes(q);
|
||||
})
|
||||
.map((p) => (
|
||||
<div className="max-h-48 overflow-y-auto overscroll-contain">
|
||||
<button
|
||||
key={p.id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 whitespace-nowrap",
|
||||
p.id === issue.projectId && "bg-accent"
|
||||
!issue.projectId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { onUpdate({ projectId: p.id }); setProjectOpen(false); }}
|
||||
onClick={() => { onUpdate({ projectId: null }); setProjectOpen(false); }}
|
||||
>
|
||||
<span
|
||||
className="shrink-0 h-3 w-3 rounded-sm"
|
||||
style={{ backgroundColor: p.color ?? "#6366f1" }}
|
||||
/>
|
||||
{p.name}
|
||||
No project
|
||||
</button>
|
||||
))}
|
||||
{(projects ?? [])
|
||||
.filter((p) => {
|
||||
if (!projectSearch.trim()) return true;
|
||||
const q = projectSearch.toLowerCase();
|
||||
return p.name.toLowerCase().includes(q);
|
||||
})
|
||||
.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 whitespace-nowrap",
|
||||
p.id === issue.projectId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { onUpdate({ projectId: p.id }); setProjectOpen(false); }}
|
||||
>
|
||||
<span
|
||||
className="shrink-0 h-3 w-3 rounded-sm"
|
||||
style={{ backgroundColor: p.color ?? "#6366f1" }}
|
||||
/>
|
||||
{p.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{issue.projectId && (
|
||||
|
||||
@@ -214,7 +214,7 @@ export function IssuesList({
|
||||
<span className="hidden sm:inline">New Issue</span>
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-0.5 sm:gap-1">
|
||||
<div className="flex items-center gap-0.5 sm:gap-1 shrink-0">
|
||||
{/* Filter */}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
@@ -396,35 +396,37 @@ export function NewIssueDialog() {
|
||||
onChange={(e) => setAssigneeSearch(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
!assigneeId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setAssigneeId(""); setAssigneeOpen(false); }}
|
||||
>
|
||||
No assignee
|
||||
</button>
|
||||
{(agents ?? [])
|
||||
.filter((a) => a.status !== "terminated")
|
||||
.filter((a) => {
|
||||
if (!assigneeSearch.trim()) return true;
|
||||
const q = assigneeSearch.toLowerCase();
|
||||
return a.name.toLowerCase().includes(q);
|
||||
})
|
||||
.map((a) => (
|
||||
<div className="max-h-48 overflow-y-auto overscroll-contain">
|
||||
<button
|
||||
key={a.id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
a.id === assigneeId && "bg-accent"
|
||||
!assigneeId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setAssigneeId(a.id); setAssigneeOpen(false); }}
|
||||
onClick={() => { setAssigneeId(""); setAssigneeOpen(false); }}
|
||||
>
|
||||
<AgentIcon icon={a.icon} className="shrink-0 h-3 w-3 text-muted-foreground" />
|
||||
{a.name}
|
||||
No assignee
|
||||
</button>
|
||||
))}
|
||||
{(agents ?? [])
|
||||
.filter((a) => a.status !== "terminated")
|
||||
.filter((a) => {
|
||||
if (!assigneeSearch.trim()) return true;
|
||||
const q = assigneeSearch.toLowerCase();
|
||||
return a.name.toLowerCase().includes(q);
|
||||
})
|
||||
.map((a) => (
|
||||
<button
|
||||
key={a.id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
a.id === assigneeId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setAssigneeId(a.id); setAssigneeOpen(false); }}
|
||||
>
|
||||
<AgentIcon icon={a.icon} className="shrink-0 h-3 w-3 text-muted-foreground" />
|
||||
{a.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -449,31 +451,33 @@ export function NewIssueDialog() {
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit min-w-[11rem] p-1" align="start">
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 whitespace-nowrap",
|
||||
!projectId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setProjectId(""); setProjectOpen(false); }}
|
||||
>
|
||||
No project
|
||||
</button>
|
||||
{(projects ?? []).map((p) => (
|
||||
<div className="max-h-48 overflow-y-auto overscroll-contain">
|
||||
<button
|
||||
key={p.id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 whitespace-nowrap",
|
||||
p.id === projectId && "bg-accent"
|
||||
!projectId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setProjectId(p.id); setProjectOpen(false); }}
|
||||
onClick={() => { setProjectId(""); setProjectOpen(false); }}
|
||||
>
|
||||
<span
|
||||
className="shrink-0 h-3 w-3 rounded-sm"
|
||||
style={{ backgroundColor: p.color ?? "#6366f1" }}
|
||||
/>
|
||||
{p.name}
|
||||
No project
|
||||
</button>
|
||||
))}
|
||||
{(projects ?? []).map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 whitespace-nowrap",
|
||||
p.id === projectId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setProjectId(p.id); setProjectOpen(false); }}
|
||||
>
|
||||
<span
|
||||
className="shrink-0 h-3 w-3 rounded-sm"
|
||||
style={{ backgroundColor: p.color ?? "#6366f1" }}
|
||||
/>
|
||||
{p.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user