feat(FN-1179): improve dashboard core modal mobile behavior

- Update core modal CSS for mobile breakpoints across task detail, new task, settings, and git manager flows
- Align spacing, sizing, and responsive layout rules to keep modal interactions usable on small screens
- Add core-modals-mobile tests to verify mobile-specific CSS behavior and regression coverage
- Document the modal mobile adaptations in the dashboard README for maintainability
This commit is contained in:
gsxdsm
2026-04-08 12:59:39 -07:00
parent bcbe4ad286
commit ac4d8a18bc
3 changed files with 238 additions and 1 deletions

View File

@@ -169,6 +169,12 @@ In addition to the global mobile foundation, several power-user surfaces now inc
- Settings content remains independently scrollable (`.settings-content` keeps `flex: 1; min-height: 0; overflow-y: auto`) so tabs stay reachable while content scrolls.
- Settings form controls inside `.settings-content` enforce `font-size: 16px` on mobile to prevent iOS zoom-on-focus.
- **Core modals (TaskDetail, NewTask, GitManager)**
- At `@media (max-width: 768px)`, core modals use full-screen-friendly layout rules: header rows wrap, sticky action rows include safe-area bottom padding, and tab/nav strips stay horizontally scrollable.
- `TaskDetailModal` mobile overrides include wrapping title metadata (`.detail-title-row`), full-screen refine overlay sizing (`.detail-refine-modal`), and edit-form padding cleanup for embedded `TaskForm` fields.
- `NewTaskModal` / `TaskForm` mobile overrides remove desktop body max-height constraints, stack model selector rows vertically, and clamp dropdown/popover surfaces (`.dep-dropdown`, `.refine-menu--modal`) to modal bounds.
- `GitManagerModal` now applies sidebar-to-horizontal-nav and single-column status-grid layout at the shared `768px` breakpoint (with the existing `640px` block retained for extra-compact typography/sizing).
- **AgentsView**
- Board mode collapses to a single column (`.agent-board { grid-template-columns: 1fr; }`).
- Controls stack vertically (`.agent-controls` + `.agent-controls-actions`) with full-width action buttons and touch-friendly sizing.

View File

@@ -0,0 +1,98 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
const stylesPath = path.resolve(__dirname, "../../styles.css");
function getMainMobileBlock(css: string): string {
const mobileSectionStart = css.indexOf("/* === Mobile Responsive Overrides ===");
const tabletSectionStart = css.indexOf("/* === Tablet Responsive Tier", mobileSectionStart);
expect(mobileSectionStart).toBeGreaterThan(-1);
expect(tabletSectionStart).toBeGreaterThan(mobileSectionStart);
const block = css.slice(mobileSectionStart, tabletSectionStart);
expect(block).toContain("@media (max-width: 768px)");
expect(block).toContain(".modal-overlay");
expect(block).toContain(".detail-tabs");
return block;
}
describe("core modals mobile css coverage", () => {
it("TaskDetailModal: modal-actions uses safe-area inset bottom padding", () => {
const css = fs.readFileSync(stylesPath, "utf-8");
const mobileBlock = getMainMobileBlock(css);
expect(mobileBlock).toContain(".modal-actions {");
expect(mobileBlock).toContain("env(safe-area-inset-bottom, 0px)");
});
it("TaskDetailModal: detail tabs are horizontally scrollable and tabs do not shrink", () => {
const css = fs.readFileSync(stylesPath, "utf-8");
const mobileBlock = getMainMobileBlock(css);
expect(mobileBlock).toContain(".detail-tabs {");
expect(mobileBlock).toContain("overflow-x: auto;");
expect(mobileBlock).toContain(".detail-tab {");
expect(mobileBlock).toContain("flex-shrink: 0;");
});
it("TaskDetailModal: refine modal goes full-screen on mobile", () => {
const css = fs.readFileSync(stylesPath, "utf-8");
const mobileBlock = getMainMobileBlock(css);
expect(mobileBlock).toContain(".detail-refine-modal {");
expect(mobileBlock).toContain("width: 100%;");
expect(mobileBlock).toContain("max-width: 100%;");
});
it("NewTaskModal: modal body unsets desktop max-height for mobile", () => {
const css = fs.readFileSync(stylesPath, "utf-8");
const mobileBlock = getMainMobileBlock(css);
expect(mobileBlock).toContain(".new-task-modal .modal-body {");
expect(mobileBlock).toContain("max-height: unset;");
expect(mobileBlock).toContain("overflow-y: auto;");
});
it("TaskForm: model selection rows stack vertically on mobile", () => {
const css = fs.readFileSync(stylesPath, "utf-8");
const mobileBlock = getMainMobileBlock(css);
expect(mobileBlock).toContain(".model-select-row {");
expect(mobileBlock).toContain("flex-direction: column;");
expect(mobileBlock).toContain(".model-select-label {");
expect(mobileBlock).toContain("width: auto;");
expect(mobileBlock).toContain("text-align: left;");
});
it("SettingsModal: layout stacks and sidebar becomes horizontal scroll row", () => {
const css = fs.readFileSync(stylesPath, "utf-8");
const mobileBlock = getMainMobileBlock(css);
expect(mobileBlock).toContain(".settings-layout {");
expect(mobileBlock).toContain("flex-direction: column;");
expect(mobileBlock).toContain(".settings-sidebar {");
expect(mobileBlock).toContain("flex-direction: row;");
expect(mobileBlock).toContain("overflow-x: auto;");
});
it("GitManagerModal: 768px mobile block includes stacked layout rules", () => {
const css = fs.readFileSync(stylesPath, "utf-8");
const mobileBlock = getMainMobileBlock(css);
expect(mobileBlock).toContain(".gm-layout {");
expect(mobileBlock).toContain("flex-direction: column;");
expect(mobileBlock).toContain(".gm-sidebar {");
expect(mobileBlock).toContain("flex-direction: row;");
});
it("GitManagerModal: nav items keep 44px touch target on mobile", () => {
const css = fs.readFileSync(stylesPath, "utf-8");
const mobileBlock = getMainMobileBlock(css);
expect(mobileBlock).toContain(".gm-nav-item {");
expect(mobileBlock).toContain("min-height: 44px;");
});
});

View File

@@ -5701,7 +5701,7 @@ body {
.modal-actions {
flex-wrap: wrap;
gap: var(--space-sm);
padding: 12px 14px;
padding: 12px 14px calc(12px + env(safe-area-inset-bottom, 0px));
}
.modal-actions .btn {
@@ -5933,6 +5933,7 @@ body {
right: 0;
min-width: 0;
width: 100%;
max-width: 100%;
max-height: 200px;
}
@@ -5949,6 +5950,138 @@ body {
.model-combobox-option {
min-height: 44px;
}
/* Task detail modal: prevent header clipping + full-screen refine flow */
.detail-title-row {
flex-wrap: wrap;
gap: var(--space-xs);
}
.modal-header-actions {
flex-shrink: 0;
}
.detail-refine-overlay {
align-items: stretch;
justify-content: stretch;
}
.detail-refine-modal {
width: 100%;
max-width: 100%;
max-height: 100dvh;
border-radius: 0;
border: none;
}
.detail-refine-textarea {
font-size: 16px;
}
.modal-edit-form .form-group {
padding: 0;
}
/* New task + task form: stacked mobile fields and bounded popovers */
.new-task-modal {
display: flex;
flex-direction: column;
}
.new-task-modal .modal-body {
max-height: unset;
overflow-y: auto;
flex: 1;
min-height: 0;
padding: 14px 0;
}
.new-task-modal .form-group {
padding: 0 14px;
}
.task-form .dep-dropdown {
left: 0;
right: 0;
max-width: 100%;
}
.model-select-row {
flex-direction: column;
align-items: stretch;
gap: var(--space-xs);
}
.model-select-label {
width: auto;
text-align: left;
}
.refine-menu--modal {
left: 14px;
right: 14px;
}
.task-form-description-actions {
flex-wrap: wrap;
gap: var(--space-xs);
}
.task-form .checkbox-label {
flex-wrap: wrap;
}
.task-form .checkbox-label > div {
flex: 1;
min-width: 0;
}
/* Settings modal: keep model preset rows stackable on narrow screens */
.settings-preset-list {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
/* Git manager: align breakpoint behavior with global 768px mobile modal rules */
.gm-layout {
flex-direction: column;
}
.gm-sidebar {
flex-direction: row;
width: 100%;
min-width: unset;
border-right: none;
border-bottom: 1px solid var(--border);
overflow-x: auto;
-webkit-overflow-scrolling: touch;
padding: var(--space-xs) var(--space-sm);
gap: var(--space-xs);
}
.gm-nav-item {
border-left: none;
border-bottom: 2px solid transparent;
text-align: center;
justify-content: center;
min-height: 44px;
}
.gm-nav-item.active {
border-left-color: transparent;
border-bottom-color: var(--todo);
}
.gm-content {
min-height: 200px;
padding: var(--space-md);
padding-bottom: max(var(--space-md), env(safe-area-inset-bottom, 0px));
}
.gm-status-grid {
grid-template-columns: 1fr;
}
}
/* === Tablet Responsive Tier (769px1024px) === */