diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index bb26119f90..34667f283b 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -98,6 +98,19 @@ Behavior: - Nodes support manual drag repositioning with a 4px movement threshold to separate click from drag, using pointer capture and zoom-aware delta scaling for reliable tracking - Custom node positions persist per project in browser localStorage (`kb:${projectId}:fusion-plugin-dependency-graph:positions`) across refresh/project switches, and **Fit to graph** clears saved positions and restores auto-layout +## Workflow Editor + +The workflow editor opens as a full-screen modal editor for authoring custom workflows from the board's workflow selector. + +Navigation: +- Open a task or board surface that shows the workflow selector, then choose **Manage…** + +Behavior: +- Opens a workflow node editor with a workflow list/sidebar, canvas, inspector, and settings/authoring panels +- On desktop, the editor uses a multi-panel layout for editing the graph and adjacent workflow metadata +- On viewports `<=768px`, the editor switches to a full-screen mobile sheet and stacks the sidebar, canvas, inspector, and settings/authoring panels vertically so each section remains scrollable and usable on phones +- The create-workflow dialog and workflow AI authoring popover follow the same mobile full-screen/sheet pattern so they are not clipped by the editor canvas on narrow screens + ## Planning Mode Planning Mode now includes branch controls on the summary screen before you create a task. diff --git a/packages/dashboard/app/components/WorkflowFieldsPanel.css b/packages/dashboard/app/components/WorkflowFieldsPanel.css index f810741d9a..38116b2fc9 100644 --- a/packages/dashboard/app/components/WorkflowFieldsPanel.css +++ b/packages/dashboard/app/components/WorkflowFieldsPanel.css @@ -193,3 +193,18 @@ .wf-field-preview { padding-top: var(--space-xs); } + +@media (max-width: 768px) { + .wf-fields-panel { + width: 100%; + min-width: 0; + padding: var(--space-sm); + border-left: none; + border-top: 1px solid var(--border); + } + + .wf-field-row, + .wf-field-option-row { + flex-wrap: wrap; + } +} diff --git a/packages/dashboard/app/components/WorkflowNodeEditor.css b/packages/dashboard/app/components/WorkflowNodeEditor.css index de7943da73..36dfe46c51 100644 --- a/packages/dashboard/app/components/WorkflowNodeEditor.css +++ b/packages/dashboard/app/components/WorkflowNodeEditor.css @@ -1145,3 +1145,97 @@ background: var(--bg); box-shadow: var(--shadow-md); } + +@media (max-width: 768px) { + .modal-overlay:has(.wf-editor-modal), + .modal-overlay:has(.wf-create-modal) { + padding-top: 0; + align-items: stretch; + justify-content: stretch; + } + + .wf-editor-modal, + .wf-create-modal { + width: 100vw; + min-width: 0; + max-width: 100vw; + height: 100vh; + height: 100dvh; + min-height: 0; + max-height: 100dvh; + margin: 0; + border: none; + border-radius: 0; + resize: none; + flex: 1 1 auto; + } + + .wf-editor-header, + .wf-migration-notice, + .wf-editor-readonly-banner { + flex-wrap: wrap; + } + + .wf-migration-notice, + .wf-editor-readonly-banner { + gap: var(--space-sm); + } + + .wf-editor-body { + flex-direction: column; + } + + .wf-editor-sidebar { + width: 100%; + max-height: 30vh; + border-right: none; + border-bottom: 1px solid var(--border); + overflow-y: auto; + } + + .wf-editor-sidebar .wf-column-panel { + width: 100%; + min-width: 0; + border-left: none; + border-top: 1px solid var(--border); + } + + .wf-editor-body .wf-settings-panel { + width: 100%; + min-width: 0; + max-height: 50vh; + border-left: none; + border-top: 1px solid var(--border); + overflow-y: auto; + } + + .wf-editor-canvas-wrap { + flex: 1 1 auto; + min-height: 40vh; + } + + .wf-template-list { + max-height: 40vh; + } + + .wf-editor-toolbar { + overflow-x: auto; + } + + .wf-ai-panel { + position: fixed; + inset: var(--space-sm); + top: auto; + right: auto; + z-index: 30; + width: auto; + } + + .wf-editor-inspector { + width: 100%; + max-height: 40vh; + border-left: none; + border-top: 1px solid var(--border); + overflow-y: auto; + } +} diff --git a/packages/dashboard/app/components/WorkflowSelector.css b/packages/dashboard/app/components/WorkflowSelector.css index 8f0ec69d57..ecd128b7ec 100644 --- a/packages/dashboard/app/components/WorkflowSelector.css +++ b/packages/dashboard/app/components/WorkflowSelector.css @@ -46,3 +46,19 @@ .workflow-selector-manage:hover { background: var(--bg-tertiary); } + +@media (max-width: 768px) { + .workflow-selector { + flex-direction: column; + align-items: stretch; + } + + .workflow-selector select, + .workflow-selector-manage { + width: 100%; + } + + .workflow-selector-manage { + text-align: center; + } +} diff --git a/packages/dashboard/app/components/WorkflowSettingsPanel.css b/packages/dashboard/app/components/WorkflowSettingsPanel.css index 39b6866cf2..76dbea5fa9 100644 --- a/packages/dashboard/app/components/WorkflowSettingsPanel.css +++ b/packages/dashboard/app/components/WorkflowSettingsPanel.css @@ -339,3 +339,23 @@ cursor: pointer; padding: 0; } + +@media (max-width: 768px) { + .wf-settings-panel-wrap { + flex-direction: column; + } + + .wf-settings-panel { + width: 100%; + min-width: 0; + padding: var(--space-sm); + border-left: none; + border-top: 1px solid var(--border); + } + + .wf-setting-row, + .wf-setting-option-row, + .wf-settings-orphaned-row { + flex-wrap: wrap; + } +} diff --git a/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.css.test.ts b/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.css.test.ts new file mode 100644 index 0000000000..bbd6bbf7ac --- /dev/null +++ b/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.css.test.ts @@ -0,0 +1,105 @@ +import { readFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { describe, expect, it } from "vitest"; +import { loadAllAppCssBaseOnly } from "../../test/cssFixture"; + +const COMPONENTS_DIR = resolve(__dirname, ".."); + +function readComponentCss(fileName: string): string { + return readFileSync(join(COMPONENTS_DIR, fileName), "utf-8"); +} + +function extractMediaBlocks(css: string, query: string): string[] { + const blocks: string[] = []; + let cursor = 0; + while (cursor < css.length) { + const start = css.indexOf(`@media ${query}`, cursor); + if (start < 0) break; + const open = css.indexOf("{", start); + let depth = 1; + let i = open + 1; + while (i < css.length && depth > 0) { + if (css[i] === "{") depth += 1; + else if (css[i] === "}") depth -= 1; + i += 1; + } + blocks.push(css.slice(open + 1, i - 1)); + cursor = i; + } + return blocks; +} + +function findRule(blocks: string[], selector: RegExp): string { + const rule = blocks.map((block) => block.match(selector)?.[0] ?? "").find(Boolean) ?? ""; + expect(rule).toBeTruthy(); + return rule; +} + +describe("WorkflowNodeEditor mobile CSS contract", () => { + it("FN-5992 preserves desktop editor min-width while adding full-screen mobile overrides", () => { + const baseCss = loadAllAppCssBaseOnly(); + const editorCss = readComponentCss("WorkflowNodeEditor.css"); + const mobileBlocks = extractMediaBlocks(editorCss, "(max-width: 768px)"); + + expect(baseCss).toMatch(/\.wf-editor-modal\s*\{[^}]*min-width\s*:\s*640px\s*;/); + + const editorModalRule = findRule(mobileBlocks, /\.wf-editor-modal,\s*\.wf-create-modal\s*\{[^}]*\}/); + expect(editorModalRule).toMatch(/width\s*:\s*100vw\s*;/); + expect(editorModalRule).toMatch(/height\s*:\s*100dvh\s*;/); + expect(editorModalRule).toMatch(/border-radius\s*:\s*0\s*;/); + expect(editorModalRule).toMatch(/resize\s*:\s*none\s*;/); + + const sidebarRule = findRule(mobileBlocks, /\.wf-editor-sidebar\s*\{[^}]*\}/); + expect(sidebarRule).toMatch(/width\s*:\s*100%\s*;/); + + const inspectorRule = findRule(mobileBlocks, /\.wf-editor-inspector\s*\{[^}]*\}/); + expect(inspectorRule).toMatch(/width\s*:\s*100%\s*;/); + + const settingsRule = findRule(mobileBlocks, /\.wf-editor-body \.wf-settings-panel\s*\{[^}]*\}/); + expect(settingsRule).toMatch(/width\s*:\s*100%\s*;/); + expect(settingsRule).toMatch(/min-width\s*:\s*0\s*;/); + + const canvasWrapRule = findRule(mobileBlocks, /\.wf-editor-canvas-wrap\s*\{[^}]*\}/); + expect(canvasWrapRule).toMatch(/min-height\s*:\s*40vh\s*;/); + }); + + it("FN-5992 covers create dialog and AI panel mobile overlays", () => { + const editorCss = readComponentCss("WorkflowNodeEditor.css"); + const mobileBlocks = extractMediaBlocks(editorCss, "(max-width: 768px)"); + + const overlayRule = findRule(mobileBlocks, /\.modal-overlay:has\(\.wf-editor-modal\),\s*\.modal-overlay:has\(\.wf-create-modal\)\s*\{[^}]*\}/); + expect(overlayRule).toMatch(/padding-top\s*:\s*0\s*;/); + expect(overlayRule).toMatch(/align-items\s*:\s*stretch\s*;/); + + const templateListRule = findRule(mobileBlocks, /\.wf-template-list\s*\{[^}]*\}/); + expect(templateListRule).toMatch(/max-height\s*:\s*40vh\s*;/); + + const aiPanelRule = findRule(mobileBlocks, /\.wf-ai-panel\s*\{[^}]*\}/); + expect(aiPanelRule).toMatch(/position\s*:\s*fixed\s*;/); + expect(aiPanelRule).toMatch(/inset\s*:\s*var\(--space-sm\)\s*;/); + expect(aiPanelRule).toMatch(/z-index\s*:\s*30\s*;/); + }); + + it("FN-5992 adds standalone mobile workflow panel overrides", () => { + const settingsCss = readComponentCss("WorkflowSettingsPanel.css"); + const fieldsCss = readComponentCss("WorkflowFieldsPanel.css"); + const selectorCss = readComponentCss("WorkflowSelector.css"); + + const settingsMobile = extractMediaBlocks(settingsCss, "(max-width: 768px)"); + const settingsRule = findRule(settingsMobile, /\.wf-settings-panel\s*\{[^}]*\}/); + expect(settingsRule).toMatch(/width\s*:\s*100%\s*;/); + expect(settingsRule).toMatch(/min-width\s*:\s*0\s*;/); + + const fieldsMobile = extractMediaBlocks(fieldsCss, "(max-width: 768px)"); + const fieldsRule = findRule(fieldsMobile, /\.wf-fields-panel\s*\{[^}]*\}/); + expect(fieldsRule).toMatch(/width\s*:\s*100%\s*;/); + expect(fieldsRule).toMatch(/min-width\s*:\s*0\s*;/); + + const selectorMobile = extractMediaBlocks(selectorCss, "(max-width: 768px)"); + const selectorRule = findRule(selectorMobile, /\.workflow-selector\s*\{[^}]*\}/); + expect(selectorRule).toMatch(/flex-direction\s*:\s*column\s*;/); + + const manageRule = findRule(selectorMobile, /\.workflow-selector select,\s*\.workflow-selector-manage\s*\{[^}]*\}/); + expect(manageRule).toMatch(/width\s*:\s*100%\s*;/); + }); +});