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");