feat(FN-3061): align remaining compact icon actions with shared sizing

Fixes FN-3061 aligning compact icon action sizing across Column, PiExtensionsManager, and ScheduleStepsEditor components for consistent UI dimensions.

Fusion-Task-Id: FN-3061
This commit is contained in:
Fusion
2026-05-01 13:00:37 -07:00
committed by gsxdsm
parent a34c819514
commit 1c0a5dfd6d
25 changed files with 182 additions and 75 deletions

View File

@@ -10,8 +10,26 @@ const hiddenDistRoot = join(workspaceRoot, `.tmp-fn-vitest-workspace-resolution-
const internalPackages = ["core", "engine", "dashboard"] as const;
const movedDistDirs: Array<{ from: string; to: string }> = [];
function rmSyncWithRetry(path: string) {
for (let attempt = 0; attempt < 5; attempt++) {
try {
rmSync(path, { recursive: true, force: true });
return;
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code !== "ENOTEMPTY" && code !== "EPERM" && code !== "EEXIST") {
throw error;
}
if (attempt === 4) {
throw error;
}
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20 * (attempt + 1));
}
}
}
function hideInternalPackageDistDirs() {
rmSync(hiddenDistRoot, { recursive: true, force: true });
rmSyncWithRetry(hiddenDistRoot);
mkdirSync(hiddenDistRoot, { recursive: true });
for (const pkg of internalPackages) {
@@ -22,7 +40,7 @@ function hideInternalPackageDistDirs() {
const hiddenPath = join(hiddenDistRoot, `${pkg}-dist`);
if (existsSync(hiddenPath)) {
rmSync(hiddenPath, { recursive: true, force: true });
rmSyncWithRetry(hiddenPath);
}
renameSync(distPath, hiddenPath);
movedDistDirs.push({ from: distPath, to: hiddenPath });
@@ -37,13 +55,13 @@ function restoreInternalPackageDistDirs() {
}
if (existsSync(from)) {
rmSync(from, { recursive: true, force: true });
rmSyncWithRetry(from);
}
renameSync(to, from);
}
movedDistDirs.length = 0;
rmSync(hiddenDistRoot, { recursive: true, force: true });
rmSyncWithRetry(hiddenDistRoot);
}
describe("CLI Vitest workspace resolution", () => {

View File

@@ -614,7 +614,7 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
{/* Footer with agent ID */}
<div className="agent-detail-footer">
<button className="btn-icon" onClick={copyAgentId} title="Copy Agent ID">
<Copy size={14} />
<Copy />
</button>
<span className="agent-detail-id" onClick={copyAgentId}>
{agent.id}

View File

@@ -144,7 +144,7 @@ export function ChangesDiffModal({
title="Previous file (Ctrl+↑)"
aria-label="Previous file"
>
<ChevronLeft size={14} />
<ChevronLeft />
</button>
<span className="changes-nav-indicator" aria-live="polite">
{selectedIndex !== null
@@ -160,7 +160,7 @@ export function ChangesDiffModal({
title="Next file (Ctrl+↓)"
aria-label="Next file"
>
<ChevronRight size={14} />
<ChevronRight />
</button>
</div>
)}

View File

@@ -309,7 +309,7 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
title="Archive all done tasks"
aria-label="Archive all done tasks"
>
<Archive size={16} />
<Archive />
</button>
)}
{isArchived && onToggleCollapse && (
@@ -319,6 +319,7 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
title={collapsed ? "Expand archived tasks" : "Collapse archived tasks"}
aria-label={collapsed ? "Expand archived tasks" : "Collapse archived tasks"}
>
{/* Directional chevrons stay explicit for clearer collapsed-state affordance in compact headers. */}
{collapsed ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
</button>
)}
@@ -334,7 +335,7 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
title="Column actions"
disabled={isMenuBusy}
>
<MoreVertical size={16} />
<MoreVertical />
</button>
{isMenuOpen && (
<div className="column-menu-popover" role="menu">

View File

@@ -552,7 +552,7 @@ export function DevServerView({ addToast, projectId }: DevServerViewProps) {
disabled={!effectivePreviewUrl}
data-testid="devserver-preview-open-tab"
>
<ExternalLink size={14} />
<ExternalLink />
</button>
<button
type="button"
@@ -562,7 +562,7 @@ export function DevServerView({ addToast, projectId }: DevServerViewProps) {
disabled={!effectivePreviewUrl}
data-testid="devserver-preview-refresh"
>
<RefreshCw size={14} />
<RefreshCw />
</button>
</div>
</div>

View File

@@ -400,7 +400,7 @@ export function MobileNavBar({
<div className="mobile-more-submenu" role="menu" aria-label="Scripts submenu">
{scriptsLoading ? (
<div className="mobile-more-submenu-loading" data-testid="mobile-more-scripts-loading">
<Loader2 size={14} className="animate-spin" />
<Loader2 className="animate-spin" />
<span>Loading scripts</span>
</div>
) : scriptEntries.length > 0 ? (
@@ -417,7 +417,7 @@ export function MobileNavBar({
setIsScriptsSubmenuOpen(false);
}}
>
<Play size={14} />
<Play />
<span>{name}</span>
</button>
))}
@@ -432,7 +432,7 @@ export function MobileNavBar({
onOpenScripts();
}}
>
<FileCode size={14} />
<FileCode />
<span>Manage Scripts</span>
</button>
)}
@@ -449,7 +449,7 @@ export function MobileNavBar({
onOpenScripts();
}}
>
<FileCode size={14} />
<FileCode />
<span>No scripts add one</span>
</button>
)
@@ -609,7 +609,7 @@ export function MobileNavBar({
handleMoreAction(() => onChangeView("dev-server"));
}}
>
<Monitor size={14} />
<Monitor />
<span>Dev Server</span>
</button>
)}
@@ -621,7 +621,7 @@ export function MobileNavBar({
data-testid="mobile-more-item-todos"
onClick={() => handleMoreAction(() => onChangeView("todos"))}
>
<CheckSquare size={14} />
<CheckSquare />
<span>Todos</span>
</button>
)}

View File

@@ -224,7 +224,7 @@ export function PiExtensionsManager({ addToast, projectId }: PiExtensionsManager
title={`Remove ${path}`}
aria-label={`Remove ${path}`}
>
<X size={12} />
<X />
</button>
</span>
))}
@@ -421,7 +421,7 @@ export function PiExtensionsManager({ addToast, projectId }: PiExtensionsManager
disabled={extensionsLoading}
title="Refresh extensions"
>
<RefreshCw size={14} className={extensionsLoading ? "spin" : ""} />
<RefreshCw className={extensionsLoading ? "spin" : ""} />
</button>
</div>
<p className="pi-ext-description">

View File

@@ -641,9 +641,6 @@
display: flex;
flex-direction: column;
gap: var(--space-md);
max-height: 40vh;
overflow-y: auto;
padding-right: var(--space-xs);
}
.planning-option {

View File

@@ -190,7 +190,7 @@ export function ProjectDetectionResults({
title="Edit name"
disabled={isRegistering}
>
<Edit2 size={12} />
<Edit2 />
</button>
</div>
)}

View File

@@ -202,7 +202,7 @@ export function RoutineCard({ routine, onEdit, onDelete, onRun, onToggle, runnin
title={running ? "Running…" : "Run now"}
aria-label={running ? "Running…" : `Run ${routine.name} now`}
>
{running ? <Loader2 size={14} className="spinner" /> : <Play size={14} />}
{running ? <Loader2 className="spinner" /> : <Play />}
</button>
<button
className="btn-icon"
@@ -211,7 +211,7 @@ export function RoutineCard({ routine, onEdit, onDelete, onRun, onToggle, runnin
aria-label={routine.enabled ? `Disable ${routine.name}` : `Enable ${routine.name}`}
aria-pressed={routine.enabled}
>
{routine.enabled ? <Pause size={14} /> : <Play size={14} />}
{routine.enabled ? <Pause /> : <Play />}
</button>
<button
className="btn-icon"
@@ -219,7 +219,7 @@ export function RoutineCard({ routine, onEdit, onDelete, onRun, onToggle, runnin
title="Edit"
aria-label={`Edit ${routine.name}`}
>
<Pencil size={14} />
<Pencil />
</button>
<button
className="btn-icon"
@@ -227,7 +227,7 @@ export function RoutineCard({ routine, onEdit, onDelete, onRun, onToggle, runnin
title="Delete"
aria-label={`Delete ${routine.name}`}
>
<Trash2 size={14} />
<Trash2 />
</button>
</div>
</div>

View File

@@ -194,7 +194,7 @@ export function ScheduleCard({ schedule, onEdit, onDelete, onRun, onToggle, runn
title={running ? "Running…" : "Run now"}
aria-label={running ? "Running…" : `Run ${schedule.name} now`}
>
<Play size={14} />
<Play />
</button>
<button
className="btn-icon"
@@ -203,7 +203,7 @@ export function ScheduleCard({ schedule, onEdit, onDelete, onRun, onToggle, runn
aria-label={schedule.enabled ? `Disable ${schedule.name}` : `Enable ${schedule.name}`}
aria-pressed={schedule.enabled}
>
{schedule.enabled ? <Pause size={14} /> : <Play size={14} />}
{schedule.enabled ? <Pause /> : <Play />}
</button>
<button
className="btn-icon"
@@ -211,7 +211,7 @@ export function ScheduleCard({ schedule, onEdit, onDelete, onRun, onToggle, runn
title="Edit"
aria-label={`Edit ${schedule.name}`}
>
<Pencil size={14} />
<Pencil />
</button>
<button
className="btn-icon"
@@ -219,7 +219,7 @@ export function ScheduleCard({ schedule, onEdit, onDelete, onRun, onToggle, runn
title="Delete"
aria-label={`Delete ${schedule.name}`}
>
<Trash2 size={14} />
<Trash2 />
</button>
</div>
</div>

View File

@@ -421,7 +421,7 @@ export function ScheduleStepsEditor({ steps, onChange, onEditingChange }: Schedu
title="Move up"
aria-label={`Move ${step.name} up`}
>
<ChevronUp size={14} />
<ChevronUp />
</button>
<button
type="button"
@@ -431,7 +431,7 @@ export function ScheduleStepsEditor({ steps, onChange, onEditingChange }: Schedu
title="Move down"
aria-label={`Move ${step.name} down`}
>
<ChevronDown size={14} />
<ChevronDown />
</button>
<button
type="button"
@@ -440,7 +440,7 @@ export function ScheduleStepsEditor({ steps, onChange, onEditingChange }: Schedu
title="Edit"
aria-label={`Edit ${step.name}`}
>
<Pencil size={14} />
<Pencil />
</button>
<button
type="button"
@@ -449,7 +449,7 @@ export function ScheduleStepsEditor({ steps, onChange, onEditingChange }: Schedu
title="Delete"
aria-label={`Delete ${step.name}`}
>
<Trash2 size={14} />
<Trash2 />
</button>
</div>
</div>

View File

@@ -715,7 +715,7 @@ export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onT
title="Move up"
aria-label="Move subtask up"
>
<ArrowUp size={14} />
<ArrowUp />
</button>
<button
type="button"
@@ -725,7 +725,7 @@ export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onT
title="Move down"
aria-label="Move subtask down"
>
<ArrowDown size={14} />
<ArrowDown />
</button>
<button type="button" className="btn btn-sm" onClick={() => removeSubtask(subtask.id)} disabled={view.type === "creating"}>
<Trash2 size={14} /> Remove

View File

@@ -43,8 +43,8 @@
.system-stats-modal__header-actions .btn-icon svg {
display: block;
width: 1rem;
height: 1rem;
width: var(--btn-icon-size);
height: var(--btn-icon-size);
}
.system-stats-modal__refresh--spinning {

View File

@@ -325,7 +325,7 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
title="Previous file"
aria-label="Previous file"
>
<ChevronLeft size={14} />
<ChevronLeft />
</button>
<span className="changes-nav-indicator" aria-live="polite">
{currentFileIndex !== null ? `${currentFileIndex + 1}/${files.length}` : `—/${files.length}`}
@@ -337,7 +337,7 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
title="Next file"
aria-label="Next file"
>
<ChevronRight size={14} />
<ChevronRight />
</button>
</div>
)}
@@ -364,7 +364,7 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
title="Expand to full-screen diff view"
aria-label="Expand diff view"
>
<Maximize2 size={14} />
<Maximize2 />
</button>
</div>
</div>

View File

@@ -1168,7 +1168,7 @@ export function TaskForm({
data-testid={`workflow-step-move-up-${stepId}`}
title="Move up"
>
<ChevronUp size={14} />
<ChevronUp />
</button>
<button
type="button"
@@ -1178,7 +1178,7 @@ export function TaskForm({
data-testid={`workflow-step-move-down-${stepId}`}
title="Move down"
>
<ChevronDown size={14} />
<ChevronDown />
</button>
<button
type="button"
@@ -1188,7 +1188,7 @@ export function TaskForm({
data-testid={`workflow-step-remove-${stepId}`}
title="Remove"
>
<X size={14} />
<X />
</button>
</div>
</div>

View File

@@ -324,7 +324,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
aria-label="Add list"
data-testid="add-list-button"
>
<Plus size={14} />
<Plus />
</button>
</div>
@@ -355,7 +355,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
}}
aria-label="Save list"
>
<Check size={14} />
<Check />
</button>
<button
type="button"
@@ -366,7 +366,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
}}
aria-label="Cancel list"
>
<X size={14} />
<X />
</button>
</div>
)}
@@ -423,7 +423,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
}}
aria-label="Save list rename"
>
<Check size={14} />
<Check />
</button>
<button
type="button"
@@ -431,7 +431,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
onClick={handleCancelRenameList}
aria-label="Cancel list rename"
>
<X size={14} />
<X />
</button>
</>
) : (
@@ -456,7 +456,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
aria-label={`Rename ${list.title}`}
data-testid={`rename-list-button-${list.id}`}
>
<Pencil size={14} />
<Pencil />
</button>
<button
type="button"
@@ -467,7 +467,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
aria-label={`Delete ${list.title}`}
data-testid={`delete-list-button-${list.id}`}
>
<Trash2 size={14} />
<Trash2 />
</button>
</div>
</>
@@ -586,7 +586,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
}}
aria-label="Save item edit"
>
<Check size={14} />
<Check />
</button>
<button
type="button"
@@ -594,7 +594,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
onClick={handleCancelEditItem}
aria-label="Cancel item edit"
>
<X size={14} />
<X />
</button>
</>
) : (
@@ -610,7 +610,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
aria-label={`Move ${item.text} up`}
data-testid={`move-up-${item.id}`}
>
<ChevronUp size={14} />
<ChevronUp />
</button>
<button
type="button"
@@ -622,7 +622,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
aria-label={`Move ${item.text} down`}
data-testid={`move-down-${item.id}`}
>
<ChevronDown size={14} />
<ChevronDown />
</button>
</div>
<button
@@ -634,7 +634,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
aria-label={`Create task from ${item.text}`}
data-testid={`create-task-from-${item.id}`}
>
<PlusCircle size={14} />
<PlusCircle />
</button>
<div
className="todo-agent-picker-trigger"
@@ -650,7 +650,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
aria-label={`Assign ${item.text} to agent`}
data-testid={`assign-agent-for-${item.id}`}
>
<Bot size={14} />
<Bot />
</button>
{showAgentPicker && activeItemForAgent === item.id && (
<div
@@ -673,7 +673,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
void handleCreateTaskAndAssign(item, agent.id);
}}
>
<Bot size={14} />
<Bot />
<span>{agent.name}</span>
<span className="todo-agent-picker-role">{agent.role}</span>
</button>
@@ -691,7 +691,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
aria-label={`Edit ${item.text}`}
data-testid={`edit-item-${item.id}`}
>
<Pencil size={14} />
<Pencil />
</button>
<button
type="button"
@@ -702,7 +702,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
aria-label={`Delete ${item.text}`}
data-testid={`delete-item-${item.id}`}
>
<Trash2 size={14} />
<Trash2 />
</button>
</>
)}

View File

@@ -391,7 +391,7 @@ export function WorkflowResultsTab({
data-testid={`workflow-step-move-up-${stepId}`}
title="Move up"
>
<ChevronUp size={14} />
<ChevronUp />
</button>
<button
type="button"
@@ -401,7 +401,7 @@ export function WorkflowResultsTab({
data-testid={`workflow-step-move-down-${stepId}`}
title="Move down"
>
<ChevronDown size={14} />
<ChevronDown />
</button>
<button
type="button"
@@ -410,7 +410,7 @@ export function WorkflowResultsTab({
data-testid={`workflow-step-remove-${stepId}`}
title="Remove"
>
<X size={14} />
<X />
</button>
</div>
</div>

View File

@@ -55,6 +55,23 @@ describe("mobile CSS foundation", () => {
expect(touchTargetMatch![1]).toContain("min-height: 44px;");
});
it("defines the shared btn-icon size variable contract", () => {
const css = loadAllAppCss();
const btnIconBlock = css.match(/\.btn-icon\s*\{([\s\S]*?)\}/);
expect(btnIconBlock).toBeTruthy();
expect(btnIconBlock![1]).toContain("--btn-icon-size: var(--icon-size-md);");
const btnIconSvgBlock = css.match(/\.btn-icon\s*>\s*svg\s*\{([\s\S]*?)\}/);
expect(btnIconSvgBlock).toBeTruthy();
expect(btnIconSvgBlock![1]).toContain("width: var(--btn-icon-size);");
expect(btnIconSvgBlock![1]).toContain("height: var(--btn-icon-size);");
const btnIconCompactBlock = css.match(/\.btn-icon\.btn-sm[\s\S]*?\{([\s\S]*?)\}/);
expect(btnIconCompactBlock).toBeTruthy();
expect(btnIconCompactBlock![1]).toContain("--btn-icon-size: var(--icon-size-sm);");
});
it("enforces 16px font size for text inputs in the main mobile media query", () => {
const css = loadAllAppCss();
const mobileSection = getMainMobileSection(css);

View File

@@ -4,6 +4,7 @@ import { RootErrorBoundary } from "./components/ErrorBoundary";
import { App } from "./App";
import { installAuthFetch } from "./auth";
import { installVersionCheck } from "./versionCheck";
import { installSwUpdate } from "./swUpdate";
import "./styles.css";
// Install the bearer-token fetch wrapper before React mounts so every API
@@ -21,13 +22,4 @@ createRoot(document.getElementById("root")!).render(
</StrictMode>,
);
if (import.meta.env.PROD && "serviceWorker" in navigator) {
navigator.serviceWorker
.register("/sw.js")
.then((registration) => {
console.log("SW registered:", registration.scope);
})
.catch((error) => {
console.log("SW registration failed:", error);
});
}
installSwUpdate();

View File

@@ -8,6 +8,12 @@ const APP_SHELL_URLS = [
"/icons/icon-512.png",
];
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});
self.addEventListener("install", (event) => {
event.waitUntil((async () => {
try {

View File

@@ -136,6 +136,8 @@ html {
--header-padding: var(--space-md) var(--space-xl);
--column-gap: var(--space-md);
--board-padding: var(--space-lg) var(--space-xl);
--icon-size-md: 16px;
--icon-size-sm: 14px;
/* Layout tokens for fixed footer (ExecutorStatusBar) */
--header-height: 57px;
@@ -704,6 +706,24 @@ body {
gap: 4px;
}
.btn-icon {
--btn-icon-size: var(--icon-size-md);
line-height: 0;
}
.btn-icon > svg {
width: var(--btn-icon-size);
height: var(--btn-icon-size);
flex-shrink: 0;
}
.btn.btn-icon.btn-sm,
.btn.btn-icon.btn--sm,
.btn-icon.btn-sm,
.btn-icon.btn--sm {
--btn-icon-size: var(--icon-size-sm);
}
/* === Button Modifiers (BEM style) === */
.btn--sm {
padding: 4px 8px;

View File

@@ -0,0 +1,46 @@
import { reloadOnce } from "./versionCheck";
function promptUpdate(worker: ServiceWorker): void {
worker.postMessage({ type: "SKIP_WAITING" });
}
function watchInstalling(installing: ServiceWorker): void {
installing.addEventListener("statechange", () => {
if (installing.state === "installed" && navigator.serviceWorker.controller) {
promptUpdate(installing);
}
});
}
export function installSwUpdate(): void {
if (!import.meta.env.PROD || !("serviceWorker" in navigator)) return;
let reloading = false;
navigator.serviceWorker.addEventListener("controllerchange", () => {
if (reloading) return;
reloading = true;
reloadOnce("service worker activated new version");
});
navigator.serviceWorker
.register("/sw.js")
.then((registration) => {
console.log("SW registered:", registration.scope);
if (registration.waiting && navigator.serviceWorker.controller) {
promptUpdate(registration.waiting);
}
if (registration.installing) {
watchInstalling(registration.installing);
}
registration.addEventListener("updatefound", () => {
const installing = registration.installing;
if (installing) watchInstalling(installing);
});
})
.catch((error) => {
console.log("SW registration failed:", error);
});
}

View File

@@ -15,7 +15,7 @@ export function consumeVersionUpdateFlag(): boolean {
return false;
}
function reloadOnce(reason: string): void {
export function reloadOnce(reason: string): void {
if (sessionStorage.getItem(RELOAD_FLAG)) {
console.warn("[versionCheck] reload already attempted, suppressing", reason);
return;