From 4879996af4d7377d3ed7a7aef925084843f7acff Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 22 Jun 2026 17:34:31 -0700 Subject: [PATCH] FN-6919: style workflow switcher like project selector Restyle the workflow switcher chrome to align with the project selector while preserving workflow-specific affordances. - Update the workflow trigger and menu CSS to use project-selector spacing, borders, hover states, shadows, and scrollbar chrome. - Keep workflow counts, selected-row tinting, edit actions, create footer, and mobile width behavior intact. - Add CSS parity coverage for the switcher trigger, menu, options, and selected state. - Add a patch changeset for the published Fusion package. Files changed: .changeset/fn-6919-workflow-switcher-style.md | 5 ++ .../dashboard/app/components/WorkflowSwitcher.css | 60 ++++++++++++++++++---- .../components/__tests__/WorkflowSwitcher.test.tsx | 46 +++++++++++++++++ 3 files changed, 102 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-6919 Fusion-Task-Lineage: df19f96a-d24e-4876-b837-41f0778b3d40 --- .changeset/fn-6919-workflow-switcher-style.md | 5 ++ .../app/components/WorkflowSwitcher.css | 60 ++++++++++++++++--- .../__tests__/WorkflowSwitcher.test.tsx | 46 ++++++++++++++ 3 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 .changeset/fn-6919-workflow-switcher-style.md diff --git a/.changeset/fn-6919-workflow-switcher-style.md b/.changeset/fn-6919-workflow-switcher-style.md new file mode 100644 index 0000000000..e0871b0e65 --- /dev/null +++ b/.changeset/fn-6919-workflow-switcher-style.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Restyle the workflow switcher trigger and dropdown to visually match the project selector. diff --git a/packages/dashboard/app/components/WorkflowSwitcher.css b/packages/dashboard/app/components/WorkflowSwitcher.css index 8428da4e68..d7ab0c4946 100644 --- a/packages/dashboard/app/components/WorkflowSwitcher.css +++ b/packages/dashboard/app/components/WorkflowSwitcher.css @@ -13,7 +13,7 @@ /* FNXC:WorkflowSwitcher 2026-06-22-00:00: -The workflow selector trigger must match the project selector trigger styling: transparent background, --radius-md border, muted text that brightens on hover with a --card-hover fill and --text-dim border, and content-driven width (no fixed min-width). Only the max-width clamp is kept so long workflow names stay bounded. +The board/list workflow dropdown sits beside the project selector in header and toolbar surfaces, so its collapsed trigger must use the same transparent ProjectSelector chrome while preserving bounded, ellipsized workflow names. */ .workflow-switcher-trigger { display: inline-flex; @@ -38,13 +38,17 @@ The workflow selector trigger must match the project selector trigger styling: t transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast); } -.workflow-switcher-trigger:hover, -.workflow-switcher-trigger[aria-expanded="true"] { +.workflow-switcher-trigger:hover { background: var(--card-hover); color: var(--text); border-color: var(--text-dim); } +.workflow-switcher-trigger[aria-expanded="true"] { + color: var(--text); + border-color: var(--text-dim); +} + .workflow-switcher-trigger:focus-visible { outline: none; box-shadow: var(--focus-ring); @@ -122,32 +126,55 @@ The switcher's inline Todo, In Progress, and Done count badges intentionally mir color: var(--text-dim); } +/* +FNXC:WorkflowSwitcher 2026-06-22-00:00: +The workflow listbox is portaled but visually remains the ProjectSelector sibling menu; keep the same rounded surface, shadow, inner padding, and thin token scrollbar while preserving the non-scrolling create footer and row-level edit buttons. +*/ .workflow-switcher-menu { position: fixed; display: flex; flex-direction: column; overflow: hidden; + padding: var(--space-sm); background: var(--surface); border: 1px solid var(--border); - border-radius: var(--radius); - box-shadow: var(--shadow); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); z-index: 1200; } .workflow-switcher-options { display: flex; flex-direction: column; + gap: calc(var(--space-xs) / 2); overflow-y: auto; overflow-x: hidden; - padding: var(--space-xs) 0; + padding: 0; + scrollbar-width: thin; + scrollbar-color: var(--text-dim) transparent; +} + +.workflow-switcher-options::-webkit-scrollbar { + width: calc(var(--space-xs) + var(--space-xs) / 2); +} + +.workflow-switcher-options::-webkit-scrollbar-track { + background: transparent; +} + +.workflow-switcher-options::-webkit-scrollbar-thumb { + background-color: var(--text-dim); + border-radius: var(--radius-pill); } .workflow-switcher-option-row { display: flex; align-items: stretch; gap: var(--space-xs); - padding: 0 var(--space-xs); + padding: 0; background: transparent; + border-radius: var(--radius-md); + transition: background var(--transition-fast); } .workflow-switcher-option-row:hover, @@ -171,13 +198,15 @@ The switcher's inline Todo, In Progress, and Done count badges intentionally mir gap: var(--space-sm); min-width: 0; flex: 1 1 auto; - padding: var(--space-sm) var(--space-md); + padding: var(--space-sm) calc(var(--space-sm) + var(--space-xs)); border: 0; + border-radius: var(--radius-md); background: transparent; color: var(--text); font: inherit; text-align: left; cursor: pointer; + transition: background var(--transition-fast); } .workflow-switcher-option:focus-visible, @@ -191,9 +220,11 @@ The switcher's inline Todo, In Progress, and Done count badges intentionally mir flex: 0 0 auto; align-self: center; color: var(--text-muted); + transition: background var(--transition-fast), color var(--transition-fast); } .workflow-switcher-edit:hover { + background: var(--card-hover); color: var(--text); } @@ -207,7 +238,8 @@ The switcher's inline Todo, In Progress, and Done count badges intentionally mir .workflow-switcher-footer { flex: 0 0 auto; - padding: var(--space-xs); + margin-top: var(--space-xs); + padding-top: var(--space-xs); border-top: 1px solid var(--border); background: var(--surface); } @@ -219,6 +251,16 @@ The switcher's inline Todo, In Progress, and Done count badges intentionally mir gap: var(--space-sm); width: 100%; min-height: calc(var(--space-lg) + var(--space-sm)); + border-radius: var(--radius-md); +} + +[data-theme="light"] .workflow-switcher-option-row--selected { + background: color-mix(in srgb, var(--todo) 10%, transparent); +} + +[data-theme="light"] .workflow-switcher-menu, +[data-theme="light"] .workflow-switcher-footer { + background: var(--surface); } @media (max-width: 768px) { diff --git a/packages/dashboard/app/components/__tests__/WorkflowSwitcher.test.tsx b/packages/dashboard/app/components/__tests__/WorkflowSwitcher.test.tsx index 974338b06c..a431ee8c25 100644 --- a/packages/dashboard/app/components/__tests__/WorkflowSwitcher.test.tsx +++ b/packages/dashboard/app/components/__tests__/WorkflowSwitcher.test.tsx @@ -132,6 +132,52 @@ describe("WorkflowSwitcher", () => { expect(switcherCss).toMatch(/@media\s*\(max-width:\s*768px\)[\s\S]*max-width:\s*calc\(100vw - var\(--space-xl\)\);/); }); + it("matches the ProjectSelector trigger and menu chrome without reverting to the old styling", () => { + /* Surface Enumeration: CSS parity covers the shared Board/ListView WorkflowSwitcher render seam, the header portal slot, desktop menu chrome, mobile max-width safety, selected/highlighted rows, count-badge preservation, and light-theme selected tint without changing behavior. */ + const css = loadAllAppCssBaseOnly(); + + const triggerRule = cssRuleFor(css, ".workflow-switcher-trigger"); + expect(triggerRule).toMatch(/background:\s*transparent/); + expect(triggerRule).toMatch(/border:\s*1px solid var\(--border\)/); + expect(triggerRule).toMatch(/border-radius:\s*var\(--radius-md\)/); + expect(triggerRule).toMatch(/padding:\s*calc\(var\(--space-xs\) \+ var\(--space-xs\) \/ 2\)/); + expect(triggerRule).toMatch(/color:\s*var\(--text-muted\)/); + expect(triggerRule).toMatch(/transition:\s*background var\(--transition-fast\), color var\(--transition-fast\), border-color var\(--transition-fast\)/); + expect(triggerRule).not.toMatch(/background:\s*var\(--bg-secondary\)/); + expect(triggerRule).not.toMatch(/border-radius:\s*var\(--radius-sm\)/); + + const triggerHoverRule = cssRuleFor(css, ".workflow-switcher-trigger:hover"); + expect(triggerHoverRule).toMatch(/background:\s*var\(--card-hover\)/); + expect(triggerHoverRule).toMatch(/color:\s*var\(--text\)/); + expect(triggerHoverRule).toMatch(/border-color:\s*var\(--text-dim\)/); + + const triggerOpenRule = cssRuleFor(css, ".workflow-switcher-trigger[aria-expanded=\"true\"]"); + expect(triggerOpenRule).toMatch(/color:\s*var\(--text\)/); + expect(triggerOpenRule).toMatch(/border-color:\s*var\(--text-dim\)/); + expect(triggerOpenRule).not.toMatch(/background:\s*var\(--bg-tertiary\)/); + + const menuRule = cssRuleFor(css, ".workflow-switcher-menu"); + expect(menuRule).toMatch(/padding:\s*var\(--space-sm\)/); + expect(menuRule).toMatch(/border-radius:\s*var\(--radius-lg\)/); + expect(menuRule).toMatch(/box-shadow:\s*var\(--shadow-lg\)/); + expect(menuRule).not.toMatch(/border-radius:\s*var\(--radius\)/); + expect(menuRule).not.toMatch(/box-shadow:\s*var\(--shadow\)/); + + const optionsRule = cssRuleFor(css, ".workflow-switcher-options"); + expect(optionsRule).toMatch(/scrollbar-width:\s*thin/); + expect(optionsRule).toMatch(/scrollbar-color:\s*var\(--text-dim\) transparent/); + expect(cssRuleFor(css, ".workflow-switcher-options::-webkit-scrollbar-thumb")).toMatch(/background-color:\s*var\(--text-dim\)/); + + const optionRowRule = cssRuleFor(css, ".workflow-switcher-option-row"); + expect(optionRowRule).toMatch(/border-radius:\s*var\(--radius-md\)/); + const optionRule = cssRuleFor(css, ".workflow-switcher-option"); + expect(optionRule).toMatch(/border-radius:\s*var\(--radius-md\)/); + expect(optionRule).toMatch(/padding:\s*var\(--space-sm\) calc\(var\(--space-sm\) \+ var\(--space-xs\)\)/); + + const selectedRule = cssRuleFor(css, ".workflow-switcher-option-row--selected"); + expect(selectedRule).toMatch(/background:\s*color-mix\(in srgb, var\(--todo\) 15%, transparent\)/); + }); + it("fires onOpen only on click-driven closed-to-open transitions", () => { const onOpen = vi.fn(); render();