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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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*;/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user