FN-8553: prevent agent card badge overlap
Keep agent identities and status badges readable across card widths. - Wrap desktop and split-sidebar card headers without permitting text overflow. - Stack identity and badge regions on narrow mobile cards. - Cover long identities and populated badge layouts with regression tests. Files changed: packages/dashboard/app/components/AgentsView.css | 38 +++++++++++++++++++++ .../app/components/__tests__/AgentsView.test.tsx | 39 ++++++++++++++++++++++ .../__tests__/agents-view-mobile.test.tsx | 12 +++++++ 3 files changed, 89 insertions(+) Fusion-Task-Id: FN-8553 Fusion-Task-Lineage: b0015f41-44e6-4cc9-8ec9-d9c0c61f7f18 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -608,10 +608,16 @@ FN-6774 removes the saturated left-edge status stripe from split-sidebar agent c
|
||||
box-shadow: inset 0 0 0 calc(var(--space-xs) / 4) var(--todo);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:AgentsView 2026-07-23-12:26:
|
||||
Agent-card identity and every visible status badge must remain in separate layout allocations at desktop, split-sidebar, and mobile widths. Let both regions shrink and wrap, while identity text breaks inside its own allocation, so long names or populated health/role/skill badges never paint over one another.
|
||||
*/
|
||||
.agent-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
@@ -619,6 +625,8 @@ FN-6774 removes the saturated left-edge status stripe from split-sidebar agent c
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-info--clickable {
|
||||
@@ -680,6 +688,7 @@ FN-6774 removes the saturated left-edge status stripe from split-sidebar agent c
|
||||
.agent-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
@@ -692,14 +701,29 @@ FN-6774 removes the saturated left-edge status stripe from split-sidebar agent c
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.agent-name,
|
||||
.agent-id {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.agent-badges {
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.agent-badges .badge {
|
||||
max-width: 100%;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.agent-approval-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -712,6 +736,7 @@ FN-6774 removes the saturated left-edge status stripe from split-sidebar agent c
|
||||
max-width: min(100%, calc(var(--space-2xl) * 6));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agent-card-chevron {
|
||||
@@ -1582,4 +1607,17 @@ AgentsView uses the measured SVG overlay as the single connector system so paren
|
||||
overscroll-behavior: contain;
|
||||
padding: var(--space-md) var(--space-md) calc(var(--space-md) + env(safe-area-inset-bottom, 0px) + var(--standalone-bottom-gap));
|
||||
}
|
||||
|
||||
.agents-view .agent-card-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.agents-view .agent-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agents-view .agent-badges {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -910,6 +910,45 @@ describe("AgentsView", () => {
|
||||
expect(css).toContain(".agents-split-sidebar .agent-card-actions .agent-card-action-label {\n display: none;\n }");
|
||||
});
|
||||
|
||||
it("keeps long agent identities and populated health badges readable in split-sidebar cards", async () => {
|
||||
const collisionAgent: Agent = {
|
||||
...mockAgents[1],
|
||||
id: "agent-marketing-manager",
|
||||
name: "Marketing Manager",
|
||||
role: "custom",
|
||||
state: "active",
|
||||
runtimeConfig: { enabled: false },
|
||||
metadata: {
|
||||
skills: ["auto::skills/../../.agents/skills/brand-strategy/SKILL.md", "auto::skills/../../.agents/skills/campaign-analytics/SKILL.md"],
|
||||
},
|
||||
};
|
||||
mockFetchAgents.mockResolvedValueOnce([collisionAgent]);
|
||||
mockFetchAgentStats.mockResolvedValueOnce({ total: 1, byState: { active: 1 }, byRole: { custom: 1 } });
|
||||
|
||||
const { container } = renderView(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
const card = await waitFor(() => {
|
||||
const renderedCard = container.querySelector<HTMLElement>(".agents-split-sidebar .agent-card");
|
||||
expect(renderedCard).toBeTruthy();
|
||||
return renderedCard!;
|
||||
});
|
||||
|
||||
expect(within(card).getByText("Marketing Manager")).toBeInTheDocument();
|
||||
expect(within(card).getByText("agent-marketing-manager")).toBeInTheDocument();
|
||||
expect(within(card).getByText("active")).toBeInTheDocument();
|
||||
expect(within(card).getByText("Heartbeat Disabled")).toBeInTheDocument();
|
||||
expect(within(card).getByText("Custom")).toBeInTheDocument();
|
||||
expect(within(card).getByText("brand-strategy")).toBeInTheDocument();
|
||||
expect(within(card).getByText("campaign-analytics")).toBeInTheDocument();
|
||||
|
||||
const css = loadAllAppCss();
|
||||
expect(css).toMatch(/\.agent-card-header\s*\{[^}]*flex-wrap:\s*wrap;[^}]*gap:\s*var\(--space-sm\);[^}]*\}/);
|
||||
expect(css).toMatch(/\.agent-info\s*\{[^}]*flex:\s*1 1 auto;[^}]*min-width:\s*0;[^}]*\}/);
|
||||
expect(css).toMatch(/\.agent-meta\s*\{[^}]*min-width:\s*0;[^}]*\}/);
|
||||
expect(css).toMatch(/\.agent-name,\s*\.agent-id\s*\{[^}]*overflow-wrap:\s*anywhere;[^}]*\}/);
|
||||
expect(css).toMatch(/\.agent-badges\s*\{[^}]*flex:\s*0 1 auto;[^}]*flex-wrap:\s*wrap;[^}]*min-width:\s*0;[^}]*\}/);
|
||||
});
|
||||
|
||||
it("opens matching detail view when clicking View Details button", async () => {
|
||||
renderView(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
|
||||
@@ -314,6 +314,18 @@ describe("agents-view mobile CSS", () => {
|
||||
expect(block).toContain("flex-wrap: wrap");
|
||||
});
|
||||
|
||||
it("stacks card identity and populated badges on narrow mobile cards", () => {
|
||||
const headerBlock = extractRuleBlock(mobileMediaBlock, ".agents-view .agent-card-header");
|
||||
expect(headerBlock).toContain("flex-direction: column");
|
||||
expect(headerBlock).toContain("align-items: stretch");
|
||||
|
||||
const infoBlock = extractRuleBlock(mobileMediaBlock, ".agents-view .agent-info");
|
||||
expect(infoBlock).toContain("min-width: 0");
|
||||
|
||||
const badgesBlock = extractRuleBlock(mobileMediaBlock, ".agents-view .agent-badges");
|
||||
expect(badgesBlock).toContain("justify-content: flex-start");
|
||||
});
|
||||
|
||||
it("defines mobile org chart sizing and pan/zoom controls rules", () => {
|
||||
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart-controls")).toContain("display: flex");
|
||||
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart-controls")).toContain("gap: var(--space-sm)");
|
||||
|
||||
Reference in New Issue
Block a user