FN-6147: polish simple editor mobile button states
Polish simple workflow editor controls for clearer mobile interaction. - add hover, focus, and active states to simple editor graph buttons and mobile tab/add controls - replace hard-coded simple editor spacing with shared spacing tokens for node labels and edge chips - add CSS contract tests covering the new button state and spacing rules Files changed: .../app/components/MobileWorkflowGraphView.css | 57 +++++++++++- .../app/components/WorkflowNodeEditor.css | 37 ++++++++ .../__tests__/MobileWorkflowGraphView.css.test.ts | 100 +++++++++++++++++++++ 3 files changed, 191 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-6147 Fusion-Task-Lineage: 5ef4baeb-ccad-4811-aada-a195ecf8d754
This commit is contained in:
@@ -33,6 +33,23 @@
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--transition-fast),
|
||||
transform var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.mobile-wf-node-main:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.mobile-wf-node-main:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring-strong);
|
||||
}
|
||||
|
||||
.mobile-wf-node-main:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.mobile-wf-node-row--selected .mobile-wf-node-main {
|
||||
@@ -43,7 +60,7 @@
|
||||
.mobile-wf-node-kind {
|
||||
min-width: 4.5rem;
|
||||
max-width: 6rem;
|
||||
padding: 2px var(--space-xs);
|
||||
padding: calc(var(--space-xs) / 2) var(--space-xs);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
@@ -56,7 +73,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
gap: calc(var(--space-xs) / 2);
|
||||
}
|
||||
|
||||
.mobile-wf-node-title,
|
||||
@@ -88,6 +105,23 @@
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--transition-fast),
|
||||
transform var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.mobile-wf-node-expand:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.mobile-wf-node-expand:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring-strong);
|
||||
}
|
||||
|
||||
.mobile-wf-node-expand:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.mobile-wf-node-meta {
|
||||
@@ -101,7 +135,7 @@
|
||||
.mobile-wf-edge-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: var(--space-xs);
|
||||
min-height: 34px;
|
||||
max-width: 100%;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
@@ -115,6 +149,23 @@
|
||||
.mobile-wf-edge-chip {
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--transition-fast),
|
||||
transform var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.mobile-wf-edge-chip:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.mobile-wf-edge-chip:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring-strong);
|
||||
}
|
||||
|
||||
.mobile-wf-edge-chip:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.mobile-wf-edge-chip--selected {
|
||||
|
||||
@@ -1484,6 +1484,10 @@
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--transition-fast),
|
||||
transform var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.wf-mobile-tab--active {
|
||||
@@ -1491,6 +1495,19 @@
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.wf-mobile-tab:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.wf-mobile-tab:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring-strong);
|
||||
}
|
||||
|
||||
.wf-mobile-tab:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.wf-mobile-panel {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
@@ -1541,6 +1558,26 @@
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
overflow-wrap: anywhere;
|
||||
transition:
|
||||
background var(--transition-fast),
|
||||
transform var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.wf-mobile-add-option:hover,
|
||||
.wf-mobile-template-option:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.wf-mobile-add-option:focus-visible,
|
||||
.wf-mobile-template-option:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring-strong);
|
||||
}
|
||||
|
||||
.wf-mobile-add-option:active,
|
||||
.wf-mobile-template-option:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.wf-mobile-template-filter {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
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 globalSelector = new RegExp(selector.source, selector.flags.includes("g") ? selector.flags : `${selector.flags}g`);
|
||||
const matches = blocks.flatMap((block) => [...block.matchAll(globalSelector)].map((match) => match[0]));
|
||||
const rule = matches.at(-1) ?? "";
|
||||
expect(rule).toBeTruthy();
|
||||
return rule;
|
||||
}
|
||||
|
||||
describe("MobileWorkflowGraphView CSS contract", () => {
|
||||
it("adds interactive states to simple editor graph buttons", () => {
|
||||
const graphCss = readComponentCss("MobileWorkflowGraphView.css");
|
||||
|
||||
const nodeMainHoverRule = findRule([graphCss], /\.mobile-wf-node-main:hover\s*\{[^}]*\}/);
|
||||
expect(nodeMainHoverRule).toMatch(/background\s*:\s*var\(--bg-tertiary\)\s*;/);
|
||||
const nodeMainFocusRule = findRule([graphCss], /\.mobile-wf-node-main:focus-visible\s*\{[^}]*\}/);
|
||||
expect(nodeMainFocusRule).toMatch(/box-shadow\s*:\s*var\(--focus-ring-strong\)\s*;/);
|
||||
const nodeMainActiveRule = findRule([graphCss], /\.mobile-wf-node-main:active\s*\{[^}]*\}/);
|
||||
expect(nodeMainActiveRule).toMatch(/transform\s*:\s*scale\(0\.97\)\s*;/);
|
||||
|
||||
const nodeExpandHoverRule = findRule([graphCss], /\.mobile-wf-node-expand:hover\s*\{[^}]*\}/);
|
||||
expect(nodeExpandHoverRule).toMatch(/background\s*:\s*var\(--bg-tertiary\)\s*;/);
|
||||
const nodeExpandFocusRule = findRule([graphCss], /\.mobile-wf-node-expand:focus-visible\s*\{[^}]*\}/);
|
||||
expect(nodeExpandFocusRule).toMatch(/box-shadow\s*:\s*var\(--focus-ring-strong\)\s*;/);
|
||||
const nodeExpandActiveRule = findRule([graphCss], /\.mobile-wf-node-expand:active\s*\{[^}]*\}/);
|
||||
expect(nodeExpandActiveRule).toMatch(/transform\s*:\s*scale\(0\.97\)\s*;/);
|
||||
|
||||
const edgeChipHoverRule = findRule([graphCss], /\.mobile-wf-edge-chip:hover\s*\{[^}]*\}/);
|
||||
expect(edgeChipHoverRule).toMatch(/background\s*:\s*var\(--bg-secondary\)\s*;/);
|
||||
const edgeChipFocusRule = findRule([graphCss], /\.mobile-wf-edge-chip:focus-visible\s*\{[^}]*\}/);
|
||||
expect(edgeChipFocusRule).toMatch(/box-shadow\s*:\s*var\(--focus-ring-strong\)\s*;/);
|
||||
const edgeChipActiveRule = findRule([graphCss], /\.mobile-wf-edge-chip:active\s*\{[^}]*\}/);
|
||||
expect(edgeChipActiveRule).toMatch(/transform\s*:\s*scale\(0\.97\)\s*;/);
|
||||
});
|
||||
|
||||
it("uses spacing tokens for edge chip gaps", () => {
|
||||
const graphCss = readComponentCss("MobileWorkflowGraphView.css");
|
||||
|
||||
expect(graphCss).not.toMatch(/gap\s*:\s*4px\s*;/);
|
||||
const edgeChipRule = findRule([graphCss], /\.mobile-wf-column-chip,\s*\.mobile-wf-edge-chip\s*\{[^}]*\}/);
|
||||
expect(edgeChipRule).toMatch(/gap\s*:\s*var\(--space-xs\)\s*;/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WorkflowNodeEditor simple editor mobile CSS contract", () => {
|
||||
it("adds interactive states to mobile add and tab buttons", () => {
|
||||
const editorCss = readComponentCss("WorkflowNodeEditor.css");
|
||||
const mobileBlocks = extractMediaBlocks(editorCss, "(max-width: 768px)");
|
||||
|
||||
const tabHoverRule = findRule(mobileBlocks, /\.wf-mobile-tab:hover\s*\{[^}]*\}/);
|
||||
expect(tabHoverRule).toMatch(/background\s*:\s*var\(--bg-tertiary\)\s*;/);
|
||||
|
||||
const addHoverRule = findRule(
|
||||
mobileBlocks,
|
||||
/\.wf-mobile-add-option:hover,\s*\.wf-mobile-template-option:hover\s*\{[^}]*\}/,
|
||||
);
|
||||
expect(addHoverRule).toMatch(/background\s*:\s*var\(--bg-tertiary\)\s*;/);
|
||||
|
||||
const addFocusRule = findRule(
|
||||
mobileBlocks,
|
||||
/\.wf-mobile-add-option:focus-visible,\s*\.wf-mobile-template-option:focus-visible\s*\{[^}]*\}/,
|
||||
);
|
||||
expect(addFocusRule).toMatch(/box-shadow\s*:\s*var\(--focus-ring-strong\)\s*;/);
|
||||
|
||||
const addActiveRule = findRule(
|
||||
mobileBlocks,
|
||||
/\.wf-mobile-add-option:active,\s*\.wf-mobile-template-option:active\s*\{[^}]*\}/,
|
||||
);
|
||||
expect(addActiveRule).toMatch(/transform\s*:\s*scale\(0\.97\)\s*;/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user