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:
gsxdsm
2026-06-09 17:56:24 -07:00
parent f26cfe0cb0
commit 1b68107eea
3 changed files with 191 additions and 3 deletions

View File

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

View File

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

View File

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