diff --git a/packages/dashboard/app/components/Header.tsx b/packages/dashboard/app/components/Header.tsx
index 11ac2f5fd1..a649aff430 100644
--- a/packages/dashboard/app/components/Header.tsx
+++ b/packages/dashboard/app/components/Header.tsx
@@ -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 && (
)}
diff --git a/packages/dashboard/app/components/MailboxModal.css b/packages/dashboard/app/components/MailboxModal.css
index 1ad97c198f..4d62583b31 100644
--- a/packages/dashboard/app/components/MailboxModal.css
+++ b/packages/dashboard/app/components/MailboxModal.css
@@ -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);
}
diff --git a/packages/dashboard/app/components/ProjectSelector.tsx b/packages/dashboard/app/components/ProjectSelector.tsx
index 576ed9eae8..c7ca51ac00 100644
--- a/packages/dashboard/app/components/ProjectSelector.tsx
+++ b/packages/dashboard/app/components/ProjectSelector.tsx
@@ -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"
>
- {currentProject?.name || t("projectSelector.selectProject", "Select Project")}
+ {currentProject?.name || t("projectSelector.projects", "Projects")}
- {t("projectSelector.viewAll", "View All Projects")}
+ {viewAllLabel ?? t("projectSelector.viewAll", "View All Projects")}
diff --git a/packages/dashboard/app/components/WorkflowNodeEditor.css b/packages/dashboard/app/components/WorkflowNodeEditor.css
index cc91760be7..a85274c4c8 100644
--- a/packages/dashboard/app/components/WorkflowNodeEditor.css
+++ b/packages/dashboard/app/components/WorkflowNodeEditor.css
@@ -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,
diff --git a/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.css.test.ts b/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.css.test.ts
index a2f9e88cd0..790ccda070 100644
--- a/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.css.test.ts
+++ b/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.css.test.ts
@@ -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");