feat(FN-1978): rename schedule actions to Automation and polish mobile layout

- Rename Schedules/Scheduled Tasks labels to Automation in header and mobile navigation actions
- Update desktop, tablet, and mobile header tests to assert the new Automation wording and behavior
- Improve mobile schedule and routine modal spacing with token-based padding/margin updates for cards, forms, and empty states
- Refresh the UX audit report to reference the Automation label in the header control inventory
This commit is contained in:
Fusion
2026-04-17 16:10:09 -07:00
committed by gsxdsm
parent 84142ef5d9
commit 437de996a5
7 changed files with 77 additions and 27 deletions

View File

@@ -57,7 +57,7 @@ This audit acknowledges and does not duplicate the following existing backlog it
### 1.1 Header Overload on Desktop
- **Component:** `packages/dashboard/app/components/Header.tsx` (lines ~200-650)
- **Current behavior:** The header displays 15+ icon buttons without labels on desktop, including: Usage, Activity Log, Mailbox, GitHub Import, Planning, Schedules, Terminal, Files, Git Manager, Nodes, Workflow Steps, Scripts, Pause, Stop, Settings, plus view toggle buttons and project selector. Users must hover over each icon to discover its function.
- **Current behavior:** The header displays 15+ icon buttons without labels on desktop, including: Usage, Activity Log, Mailbox, GitHub Import, Planning, Automation, Terminal, Files, Git Manager, Nodes, Workflow Steps, Scripts, Pause, Stop, Settings, plus view toggle buttons and project selector. Users must hover over each icon to discover its function.
- **Recommended fix:** Group related actions into collapsible sections or a hamburger menu. Primary actions (Settings, Planning, Usage) should remain visible; secondary actions (Nodes, Workflow Steps, Scripts) should move to an overflow menu. Consider a "compact mode" toggle for users who want maximum screen space.
- **Impact:** All users are affected. New users cannot discover functionality, and power users waste time finding actions.
- **Effort estimate:** M

View File

@@ -1,26 +1,30 @@
import { describe, expect, it } from "vitest";
import { readFileSync, readdirSync, statSync } from "node:fs";
import { readFileSync, readdirSync } from "node:fs";
import { join, relative } from "node:path";
const workspaceRoot = join(__dirname, "..", "..", "..", "..");
function listSourceFiles(dir: string): string[] {
const entries = readdirSync(dir);
const entries = readdirSync(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const path = join(dir, entry);
const stat = statSync(path);
const path = join(dir, entry.name);
if (stat.isDirectory()) {
if (entry === "__tests__" || entry === "dist" || entry === "node_modules") {
if (entry.isDirectory()) {
if (
entry.name === "__tests__" ||
entry.name === "dist" ||
entry.name === "node_modules" ||
entry.name.startsWith(".")
) {
continue;
}
files.push(...listSourceFiles(path));
continue;
}
if (!/\.(ts|tsx)$/.test(entry) || /\.test\.(ts|tsx)$/.test(entry)) {
if (!/\.(ts|tsx)$/.test(entry.name) || /\.test\.(ts|tsx)$/.test(entry.name)) {
continue;
}
@@ -40,13 +44,18 @@ describe("architecture hot-path contracts", () => {
];
const bareListTaskCalls: string[] = [];
const bareListTasksPattern = /\.\s*listTasks\(\)/;
for (const root of sourceRoots) {
for (const file of listSourceFiles(join(workspaceRoot, root))) {
const content = readFileSync(file, "utf-8");
const lines = content.split("\n");
if (!bareListTasksPattern.test(content)) {
continue;
}
const lines = content.split("\n");
lines.forEach((line, index) => {
if (/\.\s*listTasks\(\)/.test(line)) {
if (bareListTasksPattern.test(line)) {
bareListTaskCalls.push(`${relative(workspaceRoot, file)}:${index + 1}`);
}
});

View File

@@ -123,9 +123,9 @@ describe("tablet header controls", () => {
expect(screen.queryByTitle("Open Terminal")).toBeNull();
});
it("does not render schedules button inline on tablet", () => {
it("does not render automation button inline on tablet", () => {
renderTabletHeader({ onOpenSchedules: noop });
expect(screen.queryByTitle("Scheduled tasks")).toBeNull();
expect(screen.queryByTitle("Automation")).toBeNull();
});
it("does not render usage button inline on tablet", () => {
@@ -194,10 +194,10 @@ describe("tablet header controls", () => {
});
});
it("overflow menu contains scheduled tasks on tablet", () => {
it("overflow menu contains automation on tablet", () => {
renderTabletHeader({ onOpenSchedules: noop });
fireEvent.click(screen.getByTitle("More header actions"));
expect(screen.getByText("Scheduled Tasks")).toBeDefined();
expect(screen.getByText("Automation")).toBeDefined();
});
it("overflow menu contains usage on tablet when provided", () => {

View File

@@ -799,21 +799,21 @@ describe("Header", () => {
});
});
describe("schedules button", () => {
it("renders schedules button on desktop", () => {
describe("automation button", () => {
it("renders automation button on desktop", () => {
renderHeader({ onOpenSchedules: vi.fn() }, "desktop");
expect(screen.getByTitle("Scheduled tasks")).toBeDefined();
expect(screen.getByTitle("Automation")).toBeDefined();
});
it("does not render schedules button inline on mobile", () => {
it("does not render automation button inline on mobile", () => {
renderHeader({ onOpenSchedules: vi.fn() }, "mobile");
expect(screen.queryByTitle("Scheduled tasks")).toBeNull();
expect(screen.queryByTitle("Automation")).toBeNull();
});
it("calls onOpenSchedules when schedules button is clicked", () => {
it("calls onOpenSchedules when automation button is clicked", () => {
const onOpenSchedules = vi.fn();
renderHeader({ onOpenSchedules }, "desktop");
fireEvent.click(screen.getByTitle("Scheduled tasks"));
fireEvent.click(screen.getByTitle("Automation"));
expect(onOpenSchedules).toHaveBeenCalled();
});
@@ -822,10 +822,10 @@ describe("Header", () => {
expect(screen.getByTestId("schedules-btn")).toBeDefined();
});
it("includes scheduled tasks in overflow menu on mobile", () => {
it("includes automation in overflow menu on mobile", () => {
renderHeader({ onOpenSchedules: vi.fn() }, "mobile");
fireEvent.click(screen.getByTitle("More header actions"));
expect(screen.getByText("Scheduled Tasks")).toBeDefined();
expect(screen.getByText("Automation")).toBeDefined();
});
it("calls onOpenSchedules from mobile overflow menu", () => {

View File

@@ -829,7 +829,7 @@ export function Header({
<button
className="btn-icon"
onClick={onOpenSchedules}
title="Scheduled tasks"
title="Automation"
data-testid="schedules-btn"
>
<Clock size={16} />
@@ -1120,7 +1120,7 @@ export function Header({
data-testid="overflow-schedules-btn"
>
<Clock size={16} />
<span>Scheduled Tasks</span>
<span>Automation</span>
</button>
{/* Activity Log - in overflow on mobile */}
{onOpenActivityLog && (

View File

@@ -447,7 +447,7 @@ export function MobileNavBar({
onClick={() => handleMoreAction(onOpenSchedules)}
>
<Clock />
<span>Schedules</span>
<span>Automation</span>
</button>
<button

View File

@@ -17149,7 +17149,12 @@ html .column.drag-over * {
@media (max-width: 768px) {
.schedule-modal-content {
max-height: none;
padding: var(--space-md) 14px;
padding: var(--space-md) var(--space-lg);
}
.schedule-card {
margin: 0;
padding: var(--space-md);
}
.schedule-card-header {
@@ -17162,6 +17167,28 @@ html .column.drag-over * {
width: 100%;
justify-content: flex-end;
}
.schedule-empty-state {
padding: var(--space-xl) var(--space-lg);
}
.schedule-form {
padding-left: var(--space-lg);
padding-right: var(--space-lg);
}
.scheduling-scope-selector {
flex-shrink: 0;
}
.modal-header-actions {
flex-wrap: wrap;
gap: var(--space-sm);
}
.detail-tabs {
padding: 0 var(--space-lg);
}
}
/* ── Routines ──────────────────────────────────────────────────── */
@@ -17405,6 +17432,11 @@ html .column.drag-over * {
/* Routine mobile responsive */
@media (max-width: 768px) {
.routine-card {
margin: 0;
padding: var(--space-md);
}
.routine-card-header {
flex-wrap: wrap;
gap: var(--space-sm);
@@ -17416,6 +17448,15 @@ html .column.drag-over * {
justify-content: flex-end;
}
.routine-empty-state {
padding: var(--space-xl) var(--space-lg);
}
.routine-form {
padding-left: var(--space-lg);
padding-right: var(--space-lg);
}
.routine-trigger-type-selector {
flex-direction: column;
}