FN-6034: fix mobile workflow canvas width and project controls

Keep workflow editing usable on mobile while preserving related dashboard controls.

- allow the standalone project selector to expose a Manage Projects action in single-project layouts
- improve the mailbox resize handle contrast for better visibility
- make the workflow editor canvas and React Flow surface shrink and fill correctly without mobile horizontal overflow
- add CSS contract coverage for desktop and mobile workflow editor width behavior

Files changed:
 packages/dashboard/app/components/Header.tsx       |  4 +++-
 packages/dashboard/app/components/MailboxModal.css |  2 +-
 .../dashboard/app/components/ProjectSelector.tsx   | 15 +++--
 .../app/components/WorkflowNodeEditor.css          | 28 +++++++++
 .../__tests__/WorkflowNodeEditor.css.test.ts       | 66 ++++++++++++++++++++++
 5 files changed, 109 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-6034

Fusion-Task-Lineage: e7a784ff-188a-4538-a2be-f75484f10aa4
This commit is contained in:
gsxdsm
2026-06-09 03:39:14 -07:00
parent 3252236528
commit 43007be53f
5 changed files with 109 additions and 6 deletions

View File

@@ -836,12 +836,14 @@ export function Header({
)}
{/* Project Selector - Back button when project selected, dropdown when 2+ projects (tablet + desktop) */}
{!isMobile && projects.length >= 1 && onViewAllProjects && onSelectProject && (
{!isMobile && projects.length >= 1 && onViewAllProjects && (
<StandaloneProjectSelector
projects={projects}
currentProject={currentProject ?? null}
onViewAll={onViewAllProjects}
onSelect={onSelectProject}
allowSingleProject
viewAllLabel={t("header.manageProjects", "Manage Projects")}
/>
)}

View File

@@ -673,7 +673,7 @@
width: var(--space-sm);
flex-shrink: 0;
cursor: col-resize;
background: transparent;
background: color-mix(in srgb, var(--border) 70%, transparent);
touch-action: none;
transition: background var(--transition-fast);
}

View File

@@ -23,6 +23,8 @@ export interface ProjectSelectorProps {
onSelect?: (project: ProjectInfo) => void;
onViewAll: () => void;
recentProjectIds?: string[];
allowSingleProject?: boolean;
viewAllLabel?: string;
}
/**
@@ -77,6 +79,8 @@ export function ProjectSelector({
onSelect,
onViewAll,
recentProjectIds = [],
allowSingleProject = false,
viewAllLabel,
}: ProjectSelectorProps) {
const { t } = useTranslation("app");
const [isOpen, setIsOpen] = useState(false);
@@ -369,8 +373,9 @@ export function ProjectSelector({
);
};
// Don't render if only one project (single-project mode)
if (projects.length <= 1) {
// Standalone project switching stays hidden in single/no-project contexts unless
// a host uses the trigger as an explicit Manage Projects affordance.
if (!allowSingleProject && projects.length <= 1) {
return null;
}
@@ -384,11 +389,12 @@ export function ProjectSelector({
aria-expanded={isOpen}
aria-haspopup="listbox"
aria-label={t("projectSelector.ariaLabel", "Select project")}
title={currentProject?.name ? t("projectSelector.switchProjectTitle", "Switch project (current: {{name}})", { name: currentProject.name }) : t("projectSelector.projectsTitle", "Projects")}
data-testid="project-selector-trigger"
>
<Folder size={16} className="project-selector__trigger-icon" />
<span className="project-selector__trigger-text">
{currentProject?.name || t("projectSelector.selectProject", "Select Project")}
{currentProject?.name || t("projectSelector.projects", "Projects")}
</span>
<ChevronDown
size={14}
@@ -583,9 +589,10 @@ export function ProjectSelector({
highlightedIndex === totalItems - 1 ? "highlighted" : ""
}`}
onClick={handleViewAll}
data-testid="manage-projects-action"
>
<Grid3X3 size={14} />
<span>{t("projectSelector.viewAll", "View All Projects")}</span>
<span>{viewAllLabel ?? t("projectSelector.viewAll", "View All Projects")}</span>
</button>
</div>
</div>

View File

@@ -73,6 +73,7 @@
.wf-editor-body {
display: flex;
flex: 1;
min-width: 0;
min-height: 0;
}
@@ -537,7 +538,10 @@
.wf-editor-canvas {
flex: 1;
width: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
position: relative;
}
@@ -1230,6 +1234,9 @@
.wf-editor-body {
flex-direction: column;
width: 100%;
min-width: 0;
overflow-x: hidden;
}
.wf-editor-body--list-stage .wf-editor-sidebar {
@@ -1254,7 +1261,10 @@
.wf-editor-body--editor-stage .wf-editor-canvas-wrap {
display: flex;
flex: 1 1 auto;
width: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.wf-editor-body--editor-stage .wf-editor-canvas {
@@ -1305,9 +1315,27 @@
.wf-editor-canvas-wrap {
flex: 1 1 auto;
width: 100%;
min-width: 0;
min-height: 0;
}
.wf-editor-canvas {
width: 100%;
min-width: 0;
max-width: 100%;
overflow: hidden;
}
.wf-editor-canvas .react-flow,
.wf-editor-canvas .react-flow__renderer,
.wf-editor-canvas .react-flow__pane,
.wf-editor-canvas .react-flow__viewport {
width: 100%;
min-width: 0;
max-width: 100%;
}
.wf-editor-list-item,
.wf-editor-new,
.wf-editor-import,

View File

@@ -103,6 +103,72 @@ describe("WorkflowNodeEditor mobile CSS contract", () => {
expect(collapsedToggleRule).toMatch(/bottom\s*:\s*var\(--space-sm\)\s*;/);
});
it("FN-6034 keeps the desktop graph canvas shrinkable without removing the modal minimum", () => {
const baseCss = loadAllAppCssBaseOnly();
const desktopModalRule = findRule([baseCss], /\.wf-editor-modal\s*\{[^}]*\}/);
expect(desktopModalRule).toMatch(/min-width\s*:\s*640px\s*;/);
const bodyRule = findRule([baseCss], /\.wf-editor-body\s*\{[^}]*\}/);
expect(bodyRule).toMatch(/min-width\s*:\s*0\s*;/);
const canvasWrapRule = findRule([baseCss], /\.wf-editor-canvas-wrap\s*\{[^}]*\}/);
expect(canvasWrapRule).toMatch(/min-width\s*:\s*0\s*;/);
const canvasRule = findRule([baseCss], /\.wf-editor-canvas\s*\{[^}]*\}/);
expect(canvasRule).toMatch(/min-width\s*:\s*0\s*;/);
expect(canvasRule).toMatch(/width\s*:\s*100%\s*;/);
expect(canvasRule).toMatch(/overflow\s*:\s*hidden\s*;/);
});
it("FN-6034 makes the mobile React Flow surface fill the editor stage without horizontal overflow", () => {
const editorCss = readComponentCss("WorkflowNodeEditor.css");
const mobileBlocks = extractMediaBlocks(editorCss, "(max-width: 768px)");
const editorBodyRule = findRule(mobileBlocks, /\.wf-editor-body\s*\{[^}]*\}/);
expect(editorBodyRule).toMatch(/width\s*:\s*100%\s*;/);
expect(editorBodyRule).toMatch(/min-width\s*:\s*0\s*;/);
expect(editorBodyRule).toMatch(/overflow-x\s*:\s*hidden\s*;/);
const editorStageWrapRule = findRule(mobileBlocks, /\.wf-editor-body--editor-stage \.wf-editor-canvas-wrap\s*\{[^}]*\}/);
expect(editorStageWrapRule).toMatch(/width\s*:\s*100%\s*;/);
expect(editorStageWrapRule).toMatch(/min-width\s*:\s*0\s*;/);
expect(editorStageWrapRule).toMatch(/overflow\s*:\s*hidden\s*;/);
const canvasRule = findRule(mobileBlocks, /\.wf-editor-canvas\s*\{[^}]*\}/);
expect(canvasRule).toMatch(/width\s*:\s*100%\s*;/);
expect(canvasRule).toMatch(/min-width\s*:\s*0\s*;/);
expect(canvasRule).toMatch(/max-width\s*:\s*100%\s*;/);
expect(canvasRule).toMatch(/overflow\s*:\s*hidden\s*;/);
const reactFlowSurfaceRule = findRule(
mobileBlocks,
/\.wf-editor-canvas \.react-flow,\s*\.wf-editor-canvas \.react-flow__renderer,\s*\.wf-editor-canvas \.react-flow__pane,\s*\.wf-editor-canvas \.react-flow__viewport\s*\{[^}]*\}/,
);
expect(reactFlowSurfaceRule).toMatch(/width\s*:\s*100%\s*;/);
expect(reactFlowSurfaceRule).toMatch(/min-width\s*:\s*0\s*;/);
expect(reactFlowSurfaceRule).toMatch(/max-width\s*:\s*100%\s*;/);
});
it("FN-6034 preserves mobile staged editor visibility and inspector stacking", () => {
const editorCss = readComponentCss("WorkflowNodeEditor.css");
const mobileBlocks = extractMediaBlocks(editorCss, "(max-width: 768px)");
const listStageHiddenRule = findRule(
mobileBlocks,
/\.wf-editor-body--list-stage \.wf-editor-canvas-wrap,\s*\.wf-editor-body--list-stage \.wf-editor-inspector\s*\{[^}]*\}/,
);
expect(listStageHiddenRule).toMatch(/display\s*:\s*none\s*;/);
const editorStageSidebarRule = findRule(mobileBlocks, /\.wf-editor-body--editor-stage \.wf-editor-sidebar\s*\{[^}]*\}/);
expect(editorStageSidebarRule).toMatch(/display\s*:\s*none\s*;/);
const inspectorRule = findRule(mobileBlocks, /\.wf-editor-body--editor-stage \.wf-editor-inspector\s*\{[^}]*\}/);
expect(inspectorRule).toMatch(/width\s*:\s*100%\s*;/);
expect(inspectorRule).toMatch(/min-width\s*:\s*0\s*;/);
expect(inspectorRule).toMatch(/border-top\s*:\s*1px solid var\(--border\)\s*;/);
});
it("FN-6033 keeps workflow editor touch target increases mobile-scoped", () => {
const baseCss = loadAllAppCssBaseOnly();
const editorCss = readComponentCss("WorkflowNodeEditor.css");