feat(FN-922): replace subtask size buttons with dropdown and improve modal UX

- Replace size selection buttons (S/M/L) with a clean dropdown in subtask breakdown dialog
- Increase description textarea height for better editing experience
- Clean up orphaned CSS from removed size button styles
- Fix fragile size selection test to use proper dropdown interaction
This commit is contained in:
gsxdsm
2026-04-04 21:04:39 -07:00
parent bb97049d57
commit f12c11160e
3 changed files with 66 additions and 61 deletions

View File

@@ -92,17 +92,39 @@ describe("SubtaskBreakdownModal", () => {
await waitFor(() => expect(screen.queryByDisplayValue("First")).not.toBeInTheDocument());
});
it("renders description textarea with 8 rows", async () => {
renderModal();
await waitFor(() => expect(streamHandlers).toBeDefined());
streamHandlers.onSubtasks(SAMPLE_SUBTASKS);
const textareas = await screen.findAllByRole("textbox");
const descriptionTextarea = textareas.find((t) => t.tagName === "TEXTAREA");
expect(descriptionTextarea).toHaveAttribute("rows", "8");
});
it("changes size and dependency selection", async () => {
renderModal();
await waitFor(() => expect(streamHandlers).toBeDefined());
streamHandlers.onSubtasks(SAMPLE_SUBTASKS);
fireEvent.click(await screen.findAllByText("L").then((buttons) => buttons[0]!));
const selects = await screen.findAllByRole("combobox");
fireEvent.change(selects[0], { target: { value: "L" } });
// Use findAllByText to get all occurrences of subtask-1 and check the first one
const subtaskLabels = await screen.findAllByText("subtask-1");
expect(subtaskLabels.length).toBeGreaterThan(0);
});
it("changes size via dropdown selection", async () => {
renderModal();
await waitFor(() => expect(streamHandlers).toBeDefined());
streamHandlers.onSubtasks(SAMPLE_SUBTASKS);
const selects = await screen.findAllByRole("combobox");
expect(selects.length).toBeGreaterThan(0);
fireEvent.change(selects[0], { target: { value: "L" } });
// Verify state was updated (the API call will receive the updated value)
fireEvent.click(screen.getByText("Create Tasks"));
await waitFor(() => expect(mockCreateTasksFromBreakdown).toHaveBeenCalled());
});
it("saves via API with edited data", async () => {
renderModal();
await waitFor(() => expect(streamHandlers).toBeDefined());

View File

@@ -473,7 +473,7 @@ export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onT
<div className="form-group">
<label>Description</label>
<textarea
rows={3}
rows={8}
value={subtask.description}
onChange={(event) => updateSubtask(subtask.id, { description: event.target.value })}
disabled={view.type === "creating"}
@@ -482,19 +482,16 @@ export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onT
<div className="form-group">
<label>Size</label>
<div className="planning-size-selector">
{(["S", "M", "L"] as const).map((size) => (
<button
key={size}
type="button"
className={`planning-size-btn ${subtask.suggestedSize === size ? "selected" : ""}`}
onClick={() => updateSubtask(subtask.id, { suggestedSize: size })}
disabled={view.type === "creating"}
>
{size}
</button>
))}
</div>
<select
className="planning-size-select"
value={subtask.suggestedSize}
onChange={(event) => updateSubtask(subtask.id, { suggestedSize: event.target.value as "S" | "M" | "L" })}
disabled={view.type === "creating"}
>
<option value="S">S</option>
<option value="M">M</option>
<option value="L">L</option>
</select>
</div>
<div className="form-group">

View File

@@ -11190,48 +11190,43 @@ html .column.drag-over * {
text-decoration: underline;
}
.planning-size-selector {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--space-md);
}
.planning-size-btn {
min-width: 0;
padding: 14px 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-xs);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
cursor: pointer;
transition:
background var(--transition-fast),
border-color var(--transition-fast),
box-shadow var(--transition-fast),
transform var(--transition-fast);
}
.planning-size-btn:hover {
background: var(--card-hover);
border-color: var(--todo);
box-shadow: var(--focus-ring);
transform: translateY(-1px);
}
.planning-size-btn.selected {
background: var(--card-hover);
border-color: var(--todo);
box-shadow: inset 0 0 0 1px var(--todo);
}
.planning-size-label {
font-size: 11px;
color: var(--text-muted);
}
/* Taller description textarea in subtask breakdown */
.planning-summary-form .form-group textarea {
min-height: 120px;
resize: vertical;
}
/* Compact size dropdown */
.planning-size-select {
width: 100%;
padding: 10px 36px 10px 12px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text);
font-size: 14px;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
}
.planning-size-select:focus {
outline: none;
border-color: var(--todo);
box-shadow: var(--focus-ring);
}
.planning-size-select:hover:not(:focus) {
border-color: var(--text-muted);
}
.planning-deps-list {
display: flex;
flex-wrap: wrap;
@@ -11544,15 +11539,6 @@ html .column.drag-over * {
border-radius: var(--radius-lg);
}
.planning-size-selector {
gap: 10px;
}
.planning-size-btn {
min-height: 72px;
padding: 12px 10px;
}
.planning-deps-list {
flex-direction: column;
flex-wrap: nowrap;