feat(KB-162): add mobile responsive layout for task detail modal

- Add mobile-first CSS with full-screen modal, wrapped actions, and reduced spacing at ≤768px
- Extract inline tab styles from TaskDetailModal.tsx into CSS classes (.detail-tabs, .detail-tab, .detail-tab-active)
- Add @media query for modal, header, body, actions, and tabs responsive adjustments
- Add tests verifying modal structure, CSS class usage, and action button presence for mobile layout
This commit is contained in:
Dustin Byrne
2026-03-28 09:14:14 -04:00
parent 81ab64bdf7
commit c4f3c39c64
3 changed files with 221 additions and 21 deletions

View File

@@ -263,36 +263,16 @@ export function TaskDetailModal({
Created {new Date(task.createdAt).toLocaleDateString()} · Updated{" "}
{new Date(task.updatedAt).toLocaleDateString()}
</div>
<div className="detail-tabs" style={{ display: "flex", gap: "0", borderBottom: "1px solid var(--border, #333)", marginBottom: "12px" }}>
<div className="detail-tabs">
<button
className={`detail-tab${activeTab === "definition" ? " detail-tab-active" : ""}`}
onClick={() => setActiveTab("definition")}
style={{
padding: "8px 16px",
background: "none",
border: "none",
borderBottom: activeTab === "definition" ? "2px solid var(--accent, #7c5cbf)" : "2px solid transparent",
color: activeTab === "definition" ? "var(--text-primary, #fff)" : "var(--text-secondary, #888)",
cursor: "pointer",
fontSize: "14px",
fontWeight: activeTab === "definition" ? 600 : 400,
}}
>
Definition
</button>
<button
className={`detail-tab${activeTab === "agent-log" ? " detail-tab-active" : ""}`}
onClick={() => setActiveTab("agent-log")}
style={{
padding: "8px 16px",
background: "none",
border: "none",
borderBottom: activeTab === "agent-log" ? "2px solid var(--accent, #7c5cbf)" : "2px solid transparent",
color: activeTab === "agent-log" ? "var(--text-primary, #fff)" : "var(--text-secondary, #888)",
cursor: "pointer",
fontSize: "14px",
fontWeight: activeTab === "agent-log" ? 600 : 400,
}}
>
Agent Log
</button>

View File

@@ -620,6 +620,150 @@ describe("TaskDetailModal", () => {
});
});
describe("mobile responsive structure", () => {
it("modal container has both 'modal' and 'modal-lg' classes for responsive CSS targeting", () => {
const { container } = render(
<TaskDetailModal
task={makeTask()}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
addToast={noop}
/>,
);
const modal = container.querySelector(".modal.modal-lg");
expect(modal).toBeTruthy();
});
it("modal overlay has 'modal-overlay' and 'open' classes", () => {
const { container } = render(
<TaskDetailModal
task={makeTask()}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
addToast={noop}
/>,
);
const overlay = container.querySelector(".modal-overlay.open");
expect(overlay).toBeTruthy();
});
it("modal-actions contains the spacer div for flex layout", () => {
const { container } = render(
<TaskDetailModal
task={makeTask({ column: "in-progress" as Column })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
addToast={noop}
/>,
);
const actions = container.querySelector(".modal-actions");
expect(actions).toBeTruthy();
// Spacer div with flex: 1 separates left actions from right actions
const spacer = actions!.querySelector("div");
expect(spacer).toBeTruthy();
expect((spacer as HTMLElement).style.flex).toContain("1");
});
it("tab buttons use CSS classes instead of inline styles for responsive override", () => {
const { container } = render(
<TaskDetailModal
task={makeTask()}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
addToast={noop}
/>,
);
const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(2);
// Tabs should use class-based styling, not inline styles
expect(tabs[0].classList.contains("detail-tab")).toBe(true);
expect(tabs[0].classList.contains("detail-tab-active")).toBe(true); // Definition is default active
expect(tabs[1].classList.contains("detail-tab-active")).toBe(false);
// Verify no inline padding/fontSize (responsive CSS controls this)
expect((tabs[0] as HTMLElement).style.padding).toBe("");
expect((tabs[0] as HTMLElement).style.fontSize).toBe("");
});
it("detail-tabs container uses CSS class instead of inline styles", () => {
const { container } = render(
<TaskDetailModal
task={makeTask()}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
addToast={noop}
/>,
);
const tabsContainer = container.querySelector(".detail-tabs");
expect(tabsContainer).toBeTruthy();
// Should not have inline display/borderBottom styles — CSS class handles it
expect((tabsContainer as HTMLElement).style.display).toBe("");
expect((tabsContainer as HTMLElement).style.borderBottom).toBe("");
});
it("detail-body is present and scrollable (flex: 1 + overflow-y: auto via CSS)", () => {
const { container } = render(
<TaskDetailModal
task={makeTask()}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
addToast={noop}
/>,
);
const body = container.querySelector(".detail-body");
expect(body).toBeTruthy();
});
it("modal-actions contains Delete and Pause buttons for non-done tasks", () => {
render(
<TaskDetailModal
task={makeTask({ column: "in-progress" as Column })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
addToast={noop}
/>,
);
expect(screen.getByText("Delete")).toBeTruthy();
expect(screen.getByText("Pause")).toBeTruthy();
});
it("in-review modal-actions contains Merge & Close and Back to In Progress buttons", () => {
render(
<TaskDetailModal
task={makeTask({ column: "in-review" as Column })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
addToast={noop}
/>,
);
expect(screen.getByText("Merge & Close")).toBeTruthy();
expect(screen.getByText("Back to In Progress")).toBeTruthy();
});
});
describe("dependency dropdown search", () => {
const searchTasks: Task[] = [
{ id: "KB-010", title: "Fix login bug", description: "Users cannot log in", column: "todo" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z" },

View File

@@ -1268,3 +1268,79 @@ html, body {
color: var(--text-muted);
white-space: nowrap;
}
/* === Detail Tabs === */
.detail-tabs {
display: flex;
gap: 0;
border-bottom: 1px solid var(--border);
margin-bottom: 12px;
}
.detail-tab {
padding: 8px 16px;
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--text-muted);
cursor: pointer;
font-size: 14px;
font-weight: 400;
font-family: inherit;
transition: color var(--transition-fast), border-color var(--transition-fast);
}
.detail-tab-active {
border-bottom-color: var(--in-progress);
color: var(--text);
font-weight: 600;
}
/* === Mobile: Task Detail Modal ===
On narrow viewports (≤768px) the modal goes full-screen, action buttons wrap,
and spacing is reduced to stay usable on screens as small as 320px. */
@media (max-width: 768px) {
.modal-overlay {
padding-top: 0;
align-items: stretch;
}
.modal,
.modal-lg {
width: 100%;
max-width: 100%;
max-height: 100vh;
max-height: 100dvh;
border-radius: 0;
border: none;
}
.detail-body {
padding: 14px;
}
.detail-title {
font-size: 16px;
}
.modal-actions {
flex-wrap: wrap;
gap: 8px;
padding: 12px 14px;
}
.modal-actions .btn {
min-width: 0;
}
.modal-header {
padding: 12px 14px;
}
.detail-tab {
padding: 8px 12px;
font-size: 13px;
flex: 1;
text-align: center;
}
}