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:
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;[^}]*\}/);
|
||||
});
|
||||
});
|
||||
@@ -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} />);
|
||||
|
||||
Reference in New Issue
Block a user