diff --git a/packages/dashboard/app/components/MobileWorkflowGraphView.css b/packages/dashboard/app/components/MobileWorkflowGraphView.css index 1a8b2dc731..adb7886f5c 100644 --- a/packages/dashboard/app/components/MobileWorkflowGraphView.css +++ b/packages/dashboard/app/components/MobileWorkflowGraphView.css @@ -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 { diff --git a/packages/dashboard/app/components/WorkflowNodeEditor.css b/packages/dashboard/app/components/WorkflowNodeEditor.css index edc2085f6e..4aec798ec2 100644 --- a/packages/dashboard/app/components/WorkflowNodeEditor.css +++ b/packages/dashboard/app/components/WorkflowNodeEditor.css @@ -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 { diff --git a/packages/dashboard/app/components/__tests__/MobileWorkflowGraphView.css.test.ts b/packages/dashboard/app/components/__tests__/MobileWorkflowGraphView.css.test.ts new file mode 100644 index 0000000000..935afe0d68 --- /dev/null +++ b/packages/dashboard/app/components/__tests__/MobileWorkflowGraphView.css.test.ts @@ -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*;/); + }); +});