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:
7
.changeset/fn-7027-skill-list-truncation.md
Normal file
7
.changeset/fn-7027-skill-list-truncation.md
Normal 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.
|
||||||
@@ -192,7 +192,17 @@ The search controls at the top of the Skills list need breathing room below the
|
|||||||
flex: 1;
|
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 {
|
.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;
|
font-weight: 500;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -200,7 +210,20 @@ The search controls at the top of the Skills list need breathing room below the
|
|||||||
white-space: nowrap;
|
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 {
|
.skills-view-item-path {
|
||||||
|
max-width: 100%;
|
||||||
|
font-size: 0.85em;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
overflow: hidden;
|
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 {
|
.skills-view-item-source {
|
||||||
|
max-width: 100%;
|
||||||
|
font-size: 0.85em;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: calc(var(--space-xs) / 8);
|
letter-spacing: calc(var(--space-xs) / 8);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toggle switch */
|
/* Toggle switch */
|
||||||
|
|||||||
@@ -563,7 +563,7 @@ export function SkillsView({ projectId, addToast, onClose }: SkillsViewProps) {
|
|||||||
<div className="skills-view-item-info">
|
<div className="skills-view-item-info">
|
||||||
<span className="skills-view-item-name">
|
<span className="skills-view-item-name">
|
||||||
{isSelected ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
{isSelected ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
{skill.name}
|
<span className="skills-view-item-name-text">{skill.name}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="skills-view-item-path">{skill.relativePath}</span>
|
<span className="skills-view-item-path">{skill.relativePath}</span>
|
||||||
<span className="skills-view-item-source">{skill.metadata.source}</span>
|
<span className="skills-view-item-source">{skill.metadata.source}</span>
|
||||||
|
|||||||
@@ -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 () => {
|
it("renders toggle sliders with .skills-view-toggle-slider class", async () => {
|
||||||
render(<SkillsView addToast={mockAddToast} onClose={onClose} />);
|
render(<SkillsView addToast={mockAddToast} onClose={onClose} />);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user