feat(FN-3063): restructure todo item rows and improve action row styling

This merge brings three major changes: a `/clear` command for Chat and QuickChat with a fixed session notification banner, a complete overhaul of the Todo item row structure and styling with mobile-responsive CSS tests and documentation of the action row pattern, and a significant expansion of droid

Fusion-Task-Id: FN-3063
This commit is contained in:
Fusion
2026-05-01 13:25:28 -07:00
committed by gsxdsm
parent c729527fcc
commit 58d6dfbc14
5 changed files with 123 additions and 42 deletions

View File

@@ -214,8 +214,8 @@
.todo-item {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
flex-direction: column;
align-items: stretch;
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-sm);
gap: var(--space-sm);
@@ -227,12 +227,20 @@
background: var(--card-hover);
}
.todo-item-main-row {
display: flex;
align-items: flex-start;
gap: var(--space-sm);
min-width: 0;
}
.todo-item-checkbox {
margin: 0;
width: var(--space-lg);
height: var(--space-lg);
accent-color: var(--todo);
cursor: pointer;
flex-shrink: 0;
}
.todo-item-text {
@@ -260,15 +268,13 @@
.todo-item-actions {
display: flex;
flex-wrap: wrap;
flex-direction: row;
align-items: center;
gap: var(--space-xs);
opacity: 0;
transition: opacity var(--transition-fast);
}
.todo-item:hover .todo-item-actions,
.todo-item:focus-within .todo-item-actions {
opacity: 1;
transition: opacity var(--transition-fast);
margin-left: calc(var(--space-lg) + var(--space-sm));
}
.todo-item-reorder-btns {
@@ -433,6 +439,14 @@
opacity: 1;
}
.todo-item-main-row {
align-items: center;
}
.todo-item-actions {
margin-left: 0;
}
.todo-item-checkbox {
width: calc(var(--space-2xl) + var(--space-xs));
height: calc(var(--space-2xl) + var(--space-xs));

View File

@@ -538,44 +538,46 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
return (
<div className="todo-item" key={item.id} data-testid={`todo-item-${item.id}`}>
<input
type="checkbox"
checked={item.completed}
onChange={() => {
void toggleItem(item.id);
}}
className="todo-item-checkbox"
aria-label={`Toggle ${item.text}`}
data-testid={`toggle-item-${item.id}`}
/>
{isEditing ? (
<div className="todo-item-main-row">
<input
className="input todo-inline-edit-input"
value={editingItemText}
onChange={(event) => setEditingItemText(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
void handleSaveEditItem();
}
if (event.key === "Escape") {
handleCancelEditItem();
}
type="checkbox"
checked={item.completed}
onChange={() => {
void toggleItem(item.id);
}}
autoFocus
data-testid={`edit-item-input-${item.id}`}
className="todo-item-checkbox"
aria-label={`Toggle ${item.text}`}
data-testid={`toggle-item-${item.id}`}
/>
) : (
<button
type="button"
className={`todo-item-text${item.completed ? " todo-item-text--completed" : ""}`}
onClick={() => handleStartEditItem(item)}
>
{item.text}
</button>
)}
<div className="todo-item-actions">
{isEditing ? (
<input
className="input todo-inline-edit-input"
value={editingItemText}
onChange={(event) => setEditingItemText(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
void handleSaveEditItem();
}
if (event.key === "Escape") {
handleCancelEditItem();
}
}}
autoFocus
data-testid={`edit-item-input-${item.id}`}
/>
) : (
<button
type="button"
className={`todo-item-text${item.completed ? " todo-item-text--completed" : ""}`}
onClick={() => handleStartEditItem(item)}
>
{item.text}
</button>
)}
</div>
<div className="todo-item-actions" data-testid={`todo-item-actions-${item.id}`}>
{isEditing ? (
<>
<button

View File

@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { loadAllAppCss, loadAllAppCssBaseOnly } from "../../test/cssFixture";
describe("TodoView action row CSS contract", () => {
it("keeps todo item actions visible by default on desktop", () => {
const baseCss = loadAllAppCssBaseOnly();
expect(baseCss).toMatch(/\.todo-item-actions\s*\{[^}]*opacity:\s*1;/);
});
it("uses a dedicated action row with mobile visibility override", () => {
const css = loadAllAppCss();
expect(css).toMatch(/\.todo-item\s*\{[^}]*flex-direction:\s*column;/);
expect(css).toMatch(/\.todo-item-main-row\s*\{[^}]*display:\s*flex;/);
expect(css).toMatch(/\.todo-item-actions\s*\{[^}]*margin-left:\s*calc\(var\(--space-lg\) \+ var\(--space-sm\)\);/);
expect(css).toMatch(/@media \(max-width:\s*768px\)\s*\{[\s\S]*\.todo-item-actions\s*\{[^}]*opacity:\s*1;[^}]*\}/);
});
});

View File

@@ -466,6 +466,44 @@ describe("TodoView", () => {
expect(addToast).toHaveBeenCalledWith("Created FN-234 and assigned to Builder", "success");
});
it("renders todo item controls in a dedicated action row", () => {
render(<TodoView addToast={addToast} />);
const todoItem = screen.getByTestId("todo-item-item-1");
const actionsRow = screen.getByTestId("todo-item-actions-item-1");
expect(todoItem.firstElementChild).toHaveClass("todo-item-main-row");
expect(actionsRow).toBeInTheDocument();
expect(actionsRow).toContainElement(screen.getByTestId("move-up-item-1"));
expect(actionsRow).toContainElement(screen.getByTestId("move-down-item-1"));
expect(actionsRow).toContainElement(screen.getByTestId("create-task-from-item-1"));
expect(actionsRow).toContainElement(screen.getByTestId("assign-agent-for-item-1"));
expect(actionsRow).toContainElement(screen.getByTestId("edit-item-item-1"));
expect(actionsRow).toContainElement(screen.getByTestId("delete-item-item-1"));
});
it("keeps long item text and action controls together in the same todo item", () => {
mockUseTodoLists.mockReturnValue(
createMockTodoLists({
items: [
{
id: "item-long",
listId: "list-1",
text: "Long todo item text that previously cramped the action controls in a single row",
completed: false,
sortOrder: 0,
},
],
}),
);
render(<TodoView addToast={addToast} />);
const todoItem = screen.getByTestId("todo-item-item-long");
expect(todoItem).toContainElement(screen.getByText(/Long todo item text/i));
expect(todoItem).toContainElement(screen.getByTestId("todo-item-actions-item-long"));
});
it("error handling shows error toast", async () => {
mockCreateTask.mockRejectedValueOnce(new Error("boom"));
render(<TodoView addToast={addToast} />);