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:
@@ -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>
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user