FN-5992: improve workflow editor mobile responsiveness

Make the workflow editor and related settings panels usable on narrow screens.

- add mobile-specific full-screen layout overrides for the workflow editor, create dialog, sidebar, inspector, settings panel, and AI panel
- stack workflow selector, workflow settings, and workflow fields controls for phone-sized viewports
- document the workflow editor mobile behavior in the dashboard guide
- add CSS contract tests covering the new mobile workflow editor and panel rules

Files changed:
 docs/dashboard-guide.md                            |  13 +++
 .../app/components/WorkflowFieldsPanel.css         |  15 +++
 .../app/components/WorkflowNodeEditor.css          |  94 ++++++++++++++++++
 .../dashboard/app/components/WorkflowSelector.css  |  16 ++++
 .../app/components/WorkflowSettingsPanel.css       |  20 ++++
 .../__tests__/WorkflowNodeEditor.css.test.ts       | 105 +++++++++++++++++++++
 6 files changed, 263 insertions(+)

Fusion-Task-Id: FN-5992

Fusion-Task-Lineage: c92396dc-88e5-4da0-be3f-687e7bd10972
This commit is contained in:
gsxdsm
2026-06-07 20:36:35 -07:00
parent 312bec674d
commit 1f2c7e22dc
6 changed files with 263 additions and 0 deletions

View File

@@ -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.

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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*;/);
});
});