fix(FN-3797): restore org chart connector endpoints for wide subtrees

- Compute first/last child subtree leaf counts in AgentsView and expose them as CSS variables on org-chart child groups
- Update org chart connector offset math to anchor horizontal bars to first/last child subtree centers instead of fixed node half-widths
- Keep defaults tokenized for single-child cases and preserve vertical-layout connector behavior
- Extend dashboard CSS and component tests to assert the new connector offset variables and connector rule coverage

Fusion-Task-Id: FN-3797
This commit is contained in:
Fusion
2026-05-08 21:31:20 -07:00
committed by gsxdsm
parent 111ad7afce
commit 12ddca0dfe
5 changed files with 50 additions and 3 deletions

View File

@@ -84,6 +84,12 @@ describe("Agent CSS classes", () => {
expect(orgChartSection).toContain("--org-chart-connector-gap: var(--space-sm)");
expect(orgChartSection).toContain("--org-chart-sibling-gap: var(--space-xl)");
expect(orgChartSection).toContain("--org-chart-children-offset: calc(var(--space-lg) + var(--space-sm))");
expect(orgChartSection).toContain("--org-chart-first-child-leaves: 1");
expect(orgChartSection).toContain("--org-chart-last-child-leaves: 1");
expect(orgChartSection).toContain("--org-chart-first-child-center-offset");
expect(orgChartSection).toContain("--org-chart-last-child-center-offset");
expect(orgChartSection).toContain("left: var(--org-chart-first-child-center-offset)");
expect(orgChartSection).toContain("right: var(--org-chart-last-child-center-offset)");
expect(orgChartSection).toContain("min-height: var(--org-chart-node-width)");
expect(orgChartSection).toContain("touch-action: pan-x pan-y");
expect(orgChartSection).toContain("overflow: auto");

View File

@@ -853,6 +853,8 @@
--org-chart-connector-gap: var(--space-sm);
--org-chart-children-offset: calc(var(--space-lg) + var(--space-sm));
--org-chart-root-gap: var(--space-xl);
--org-chart-first-child-leaves: 1;
--org-chart-last-child-leaves: 1;
display: flex;
align-items: flex-start;
justify-content: flex-start;
@@ -968,6 +970,20 @@
}
.org-chart-children {
--org-chart-first-child-leaves-number: var(--org-chart-first-child-leaves, 1);
--org-chart-last-child-leaves-number: var(--org-chart-last-child-leaves, 1);
--org-chart-first-child-center-offset: calc(
(
var(--org-chart-first-child-leaves-number) * var(--org-chart-node-width) +
(var(--org-chart-first-child-leaves-number) - 1) * var(--org-chart-sibling-gap)
) / 2
);
--org-chart-last-child-center-offset: calc(
(
var(--org-chart-last-child-leaves-number) * var(--org-chart-node-width) +
(var(--org-chart-last-child-leaves-number) - 1) * var(--org-chart-sibling-gap)
) / 2
);
position: relative;
display: flex;
align-items: flex-start;
@@ -982,8 +998,8 @@
content: "";
position: absolute;
top: 0;
left: calc(var(--org-chart-node-width) / 2);
right: calc(var(--org-chart-node-width) / 2);
left: var(--org-chart-first-child-center-offset);
right: var(--org-chart-last-child-center-offset);
height: 1px;
background: var(--border);
}

View File

@@ -125,6 +125,12 @@ function OrgChartNode({
const stateNodeClass = getStateCardClass("org-chart-node-card", agent.state);
const subtreeLeafCount = getOrgChartLeafCount(node);
const nodeStyle = { "--org-chart-subtree-leaves": String(subtreeLeafCount) } as CSSProperties;
const firstChildLeafCount = children.length > 0 ? getOrgChartLeafCount(children[0]) : 1;
const lastChildLeafCount = children.length > 0 ? getOrgChartLeafCount(children[children.length - 1]) : 1;
const childrenStyle = {
"--org-chart-first-child-leaves": String(firstChildLeafCount),
"--org-chart-last-child-leaves": String(lastChildLeafCount),
} as CSSProperties;
return (
<div
@@ -162,7 +168,7 @@ function OrgChartNode({
</div>
</div>
{children.length > 0 && (
<div className="org-chart-children" role="group" aria-label={`${agent.name} employees`}>
<div className="org-chart-children" style={childrenStyle} role="group" aria-label={`${agent.name} employees`}>
{children.map((child) => (
<OrgChartNode
key={child.agent.id}

View File

@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
import { loadAllAppCss } from "../../test/cssFixture";
import { AgentsView } from "../AgentsView";
import * as apiModule from "../../api";
import type { Agent, AgentState, AgentCapability, OrgTreeNode } from "../../api";
@@ -1335,9 +1336,20 @@ describe("AgentsView", () => {
expect(leafNode.style.getPropertyValue("--org-chart-subtree-leaves")).toBe("1");
expect(rootChildren).toBeTruthy();
expect(rootChildren.className).toContain("org-chart-children");
expect(rootChildren.style.getPropertyValue("--org-chart-first-child-leaves")).toBe("1");
expect(rootChildren.style.getPropertyValue("--org-chart-last-child-leaves")).toBe("1");
expect(container.querySelectorAll(".org-chart-node--has-children").length).toBeGreaterThan(0);
});
it("uses tokenized connector edge offsets for org chart child bars", () => {
const css = loadAllAppCss();
expect(css).toContain("--org-chart-first-child-center-offset");
expect(css).toContain("--org-chart-last-child-center-offset");
expect(css).toContain("left: var(--org-chart-first-child-center-offset)");
expect(css).toContain("right: var(--org-chart-last-child-center-offset)");
expect(css).toContain(".org-chart-children > .org-chart-node::before");
});
it("switches org chart to vertical layout mode when estimated width exceeds viewport", async () => {
const clientWidthSpy = vi.spyOn(window.HTMLElement.prototype, "clientWidth", "get").mockReturnValue(320);
mockFetchOrgTree.mockResolvedValue(orgTree);

View File

@@ -300,6 +300,13 @@ describe("agents-view mobile CSS", () => {
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart")).toContain("--org-chart-node-width: calc(var(--space-2xl) * 5)");
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart")).toContain("--org-chart-sibling-gap: var(--space-sm)");
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart")).toContain("--org-chart-children-offset: var(--space-lg)");
const childrenConnectorBlock = extractRuleBlock(cssContent, ".org-chart-children::before");
expect(childrenConnectorBlock).toContain("left: var(--org-chart-first-child-center-offset)");
expect(childrenConnectorBlock).toContain("right: var(--org-chart-last-child-center-offset)");
expect(childrenConnectorBlock).not.toContain("rgba(");
const verticalConnectorBlock = extractRuleBlock(cssContent, ".agent-org-chart--vertical .org-chart-children::before");
expect(verticalConnectorBlock).toContain("left: var(--space-sm)");
expect(verticalConnectorBlock).not.toContain("rgba(");
expect(extractRuleBlock(mobileMediaBlock, ".org-chart-node-card")).toContain("padding: var(--space-sm)");
expect(extractRuleBlock(mobileMediaBlock, ".org-chart-node__badge")).toContain("font-size: calc(var(--space-sm) + var(--space-xs) * 0.625)");
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart-shell")).toContain("overflow: hidden");