diff --git a/package-lock.json b/package-lock.json
index 85e3da6..d81ca24 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"@tauri-apps/cli": "^2.1.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
+ "@testing-library/user-event": "^14.6.1",
"@types/node": "^25.6.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
@@ -1793,6 +1794,20 @@
}
}
},
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
diff --git a/package.json b/package.json
index e95d09e..378952b 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@tauri-apps/cli": "^2.1.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
+ "@testing-library/user-event": "^14.6.1",
"@types/node": "^25.6.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
diff --git a/src/components/App.test.tsx b/src/components/App.test.tsx
index 549b660..8f39d94 100644
--- a/src/components/App.test.tsx
+++ b/src/components/App.test.tsx
@@ -1,4 +1,5 @@
import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { createFixtureFocusReader } from "../api/fixtureFocusReader";
import type { FocusWriter } from "../api/focusWriter";
@@ -56,4 +57,21 @@ describe("App overlay", () => {
expect(screen.getByText("API refactor")).toBeInTheDocument();
});
});
+
+ it("add-task input calls focusWriter.appendTask with selected focus id", async () => {
+ const writer = noopFocusWriter();
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText("Customer X bug")).toBeInTheDocument();
+ });
+
+ // Click the pig to open PigDetail
+ await userEvent.click(screen.getByText("Customer X bug"));
+
+ const input = screen.getByPlaceholderText("Add task…");
+ await userEvent.type(input, "write tests{Enter}");
+
+ expect(writer.appendTask).toHaveBeenCalledWith("a", "write tests");
+ });
});
diff --git a/src/components/App.tsx b/src/components/App.tsx
index aac7414..54606ce 100644
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -31,6 +31,15 @@ export function App({ focusReader, focusWriter }: AppProps) {
}
}
+ async function handleAddTask(text: string) {
+ if (!selectedFocus) return;
+ try {
+ await focusWriter.appendTask(selectedFocus.id, text);
+ } catch {
+ // focusWriter already logs the typed error
+ }
+ }
+
return (
{pigs.map((pig) => (
@@ -53,6 +62,7 @@ export function App({ focusReader, focusWriter }: AppProps) {
viewportH={screenH}
onClose={() => setSelectedId(null)}
onClearTask={handleClearTask}
+ onAddTask={handleAddTask}
/>
)}
diff --git a/src/components/PigDetail.test.tsx b/src/components/PigDetail.test.tsx
new file mode 100644
index 0000000..50d8791
--- /dev/null
+++ b/src/components/PigDetail.test.tsx
@@ -0,0 +1,62 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi } from "vitest";
+import type { Focus } from "../types/focus";
+import { PigDetail } from "./PigDetail";
+
+const baseFocus: Focus = {
+ id: "pig-a",
+ title: "Ship it",
+ description: "",
+ tasks: [],
+};
+
+function renderDetail(overrides?: Partial>) {
+ const props = {
+ focus: baseFocus,
+ pigX: 100,
+ pigY: 100,
+ viewportW: 1920,
+ viewportH: 1080,
+ onClose: vi.fn(),
+ onClearTask: vi.fn(),
+ onAddTask: vi.fn(),
+ ...overrides,
+ };
+ render();
+ return props;
+}
+
+describe("PigDetail add-task input", () => {
+ it("renders an add task input with placeholder", () => {
+ renderDetail();
+ expect(screen.getByPlaceholderText("Add task…")).toBeInTheDocument();
+ });
+
+ it("Enter calls onAddTask with trimmed text", async () => {
+ const { onAddTask } = renderDetail();
+ const input = screen.getByPlaceholderText("Add task…");
+ await userEvent.type(input, "fix the thing{Enter}");
+ expect(onAddTask).toHaveBeenCalledWith("fix the thing");
+ });
+
+ it("Enter clears the input field", async () => {
+ renderDetail();
+ const input = screen.getByPlaceholderText("Add task…");
+ await userEvent.type(input, "some task{Enter}");
+ expect(input).toHaveValue("");
+ });
+
+ it("Enter with empty text does not call onAddTask", async () => {
+ const { onAddTask } = renderDetail();
+ const input = screen.getByPlaceholderText("Add task…");
+ await userEvent.type(input, " {Enter}");
+ expect(onAddTask).not.toHaveBeenCalled();
+ });
+
+ it("Escape closes the card", async () => {
+ const { onClose } = renderDetail();
+ await userEvent.keyboard("{Escape}");
+ expect(onClose).toHaveBeenCalled();
+ });
+});
diff --git a/src/components/PigDetail.tsx b/src/components/PigDetail.tsx
index f2c0220..ed1cb14 100644
--- a/src/components/PigDetail.tsx
+++ b/src/components/PigDetail.tsx
@@ -1,4 +1,4 @@
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { PIG_SIZE } from "../hooks/usePigMovement";
import type { Focus } from "../types/focus";
@@ -10,9 +10,10 @@ export interface PigDetailProps {
readonly viewportH: number;
readonly onClose: () => void;
readonly onClearTask: (index: number) => void;
+ readonly onAddTask: (text: string) => void;
}
-const CARD_W = 210;
+const CARD_W = 340;
const CARD_OFFSET_X = PIG_SIZE + 8;
export function PigDetail({
@@ -23,7 +24,10 @@ export function PigDetail({
viewportH,
onClose,
onClearTask,
+ onAddTask,
}: PigDetailProps) {
+ const [taskInput, setTaskInput] = useState("");
+
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
@@ -67,6 +71,18 @@ export function PigDetail({
))}
)}
+ setTaskInput(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && taskInput.trim()) {
+ onAddTask(taskInput.trim());
+ setTaskInput("");
+ }
+ }}
+ />
>
);
diff --git a/src/styles.css b/src/styles.css
index f266b3a..17e366e 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -429,10 +429,11 @@ body,
.pig-detail {
position: absolute;
pointer-events: auto;
- background: rgba(28, 28, 30, 0.96);
+ background: rgba(20, 20, 20, 1);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
- padding: 10px 12px;
+ padding: 16px;
+ min-width: 340px;
color: #f5f5f7;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
@@ -453,11 +454,13 @@ body,
.pig-detail-tasks {
list-style: none;
- margin: 0;
+ margin: 0 0 10px;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
+ max-height: 240px;
+ overflow-y: auto;
}
.pig-detail-task {
@@ -487,3 +490,24 @@ body,
.pig-detail-task-clear:hover {
opacity: 1;
}
+
+.pig-detail-add-task {
+ width: 100%;
+ margin-top: 10px;
+ background: rgba(255, 255, 255, 0.06);
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 6px;
+ color: #f5f5f7;
+ font-size: 12px;
+ padding: 5px 8px;
+ outline: none;
+ box-sizing: border-box;
+}
+
+.pig-detail-add-task::placeholder {
+ opacity: 0.45;
+}
+
+.pig-detail-add-task:focus {
+ border-color: rgba(255, 255, 255, 0.3);
+}