FN-7027: constrain skill list row text

Keep discovered skill rows within the left pane by truncating long text values.

- Wrap skill names in a truncatable text span while keeping chevrons fixed-size.
- Add ellipsis rules and compact font sizing for skill names, paths, and sources.
- Cover long, short, and empty metadata skill rows with a dashboard test.
- Add a patch changeset for the published CLI package.

Files changed:
 .changeset/fn-7027-skill-list-truncation.md        |  7 +++
 packages/dashboard/app/components/SkillsView.css   | 28 ++++++++++
 packages/dashboard/app/components/SkillsView.tsx   |  2 +-
 .../app/components/__tests__/SkillsView.test.tsx   | 64 ++++++++++++++++++++++
 4 files changed, 100 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-7027

Fusion-Task-Lineage: ae50f1aa-e58b-4374-abae-38ca28e4073a
This commit is contained in:
gsxdsm
2026-06-25 19:29:49 -07:00
parent 8754e28868
commit ea3cfeeac4
4 changed files with 100 additions and 1 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Prevent long Skills list rows from overflowing the left pane.
category: fix
dev: Constrains SkillsView discovered-skill name, path, and source rows with ellipsis truncation.

View File

@@ -192,7 +192,17 @@ The search controls at the top of the Skills list need breathing room below the
flex: 1;
}
/*
FNXC:Skills 2026-06-25-00:00:
Left Discovered Skills rows must keep compact, intentional text sizing and truncate every text line with ellipsis so long names, paths, or sources never force horizontal scroll in wide clamped, narrow dock, or mobile list layouts. The chevron remains fixed-size while only the skill-name text truncates.
*/
.skills-view-item-name {
display: flex;
align-items: center;
gap: var(--space-xs);
max-width: 100%;
min-width: 0;
font-size: 0.9em;
font-weight: 500;
color: var(--text);
overflow: hidden;
@@ -200,7 +210,20 @@ The search controls at the top of the Skills list need breathing room below the
white-space: nowrap;
}
.skills-view-item-name svg {
flex-shrink: 0;
}
.skills-view-item-name-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.skills-view-item-path {
max-width: 100%;
font-size: 0.85em;
color: var(--text-muted);
font-family: var(--font-mono);
overflow: hidden;
@@ -209,9 +232,14 @@ The search controls at the top of the Skills list need breathing room below the
}
.skills-view-item-source {
max-width: 100%;
font-size: 0.85em;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: calc(var(--space-xs) / 8);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Toggle switch */

View File

@@ -563,7 +563,7 @@ export function SkillsView({ projectId, addToast, onClose }: SkillsViewProps) {
<div className="skills-view-item-info">
<span className="skills-view-item-name">
{isSelected ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
{skill.name}
<span className="skills-view-item-name-text">{skill.name}</span>
</span>
<span className="skills-view-item-path">{skill.relativePath}</span>
<span className="skills-view-item-source">{skill.metadata.source}</span>

View File

@@ -607,6 +607,70 @@ describe("SkillsView", () => {
});
});
it("declares ellipsis truncation for long and short discovered-skill row text", async () => {
const longToken = "skill".repeat(30);
const longSkill: DiscoveredSkill = {
id: "github::skills/" + longToken,
name: longToken,
path: "/project/.fusion/skills/" + longToken,
relativePath: "skills/" + longToken + "/nested/" + longToken,
enabled: true,
metadata: {
source: "github.com/example/" + longToken + "/" + longToken,
scope: "project",
origin: "package",
},
};
const emptyMetadataSkill = {
id: "local::skills/empty-metadata",
name: "empty-metadata-skill",
path: "/project/.fusion/skills/empty-metadata",
relativePath: "",
enabled: false,
metadata: {
scope: "project",
origin: "top-level",
},
} as unknown as DiscoveredSkill;
mockFetchDiscoveredSkills.mockResolvedValue([
mockDiscoveredSkills[0],
longSkill,
emptyMetadataSkill,
]);
render(<SkillsView addToast={mockAddToast} onClose={onClose} />);
await waitFor(() => {
expect(screen.getByText(longSkill.name)).toBeTruthy();
expect(screen.getByText("test-skill")).toBeTruthy();
expect(screen.getByText("empty-metadata-skill")).toBeTruthy();
});
const assertTruncation = (element: Element | null) => {
expect(element).toBeTruthy();
const styles = getComputedStyle(element as Element);
expect(styles.overflow).toBe("hidden");
expect(styles.textOverflow).toBe("ellipsis");
expect(styles.whiteSpace).toBe("nowrap");
};
const longRow = screen.getByText(longSkill.name).closest(".skills-view-item");
const shortRow = screen.getByText("test-skill").closest(".skills-view-item");
const emptyMetadataRow = screen.getByText("empty-metadata-skill").closest(".skills-view-item");
assertTruncation(longRow?.querySelector(".skills-view-item-name-text") ?? null);
assertTruncation(longRow?.querySelector(".skills-view-item-path") ?? null);
assertTruncation(longRow?.querySelector(".skills-view-item-source") ?? null);
assertTruncation(shortRow?.querySelector(".skills-view-item-name-text") ?? null);
assertTruncation(shortRow?.querySelector(".skills-view-item-path") ?? null);
assertTruncation(shortRow?.querySelector(".skills-view-item-source") ?? null);
expect(longRow?.querySelector(".skills-view-item-name svg")).toBeTruthy();
expect(longRow?.querySelector(".skills-view-item-toggle")).toBeTruthy();
expect(emptyMetadataRow?.querySelector(".skills-view-item-path")?.textContent).toBe("");
expect(emptyMetadataRow?.querySelector(".skills-view-item-source")?.textContent).toBe("");
});
it("renders toggle sliders with .skills-view-toggle-slider class", async () => {
render(<SkillsView addToast={mockAddToast} onClose={onClose} />);