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:
@@ -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.
|
||||
|
||||
@@ -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;");
|
||||
});
|
||||
});
|
||||
@@ -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 (769px–1024px) === */
|
||||
|
||||
Reference in New Issue
Block a user