feat(FN-1177): polish mission and planning modals for mobile

- Improve Mission Manager mobile behavior with wrapped headers/actions, 44px icon touch targets, overflow guards, and safe-area padding
- Refine planning and subtask modal mobile UX with touch-friendly drag/action controls, compact progress/layout spacing, and 16px form inputs to prevent iOS zoom
- Update model selection mobile styles to use constrained dropdown sizing and larger tappable options across 768px/640px breakpoints
- Add mission-planning-modals-mobile CSS tests that assert key mobile rules for Mission Manager, Subtask Breakdown, Planning Mode, and model combobox interactions
This commit is contained in:
gsxdsm
2026-04-08 11:38:09 -07:00
parent 9314b9a8a4
commit 4c198e3473
2 changed files with 261 additions and 4 deletions

View File

@@ -0,0 +1,125 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "fs";
import { resolve } from "path";
const css = readFileSync(resolve(__dirname, "../styles.css"), "utf-8");
function extractMaxWidthMediaBlocks(maxWidthPx: number): string[] {
const marker = `@media (max-width: ${maxWidthPx}px)`;
const blocks: string[] = [];
let fromIndex = 0;
while (fromIndex < css.length) {
const mediaStart = css.indexOf(marker, fromIndex);
if (mediaStart === -1) {
break;
}
const openBrace = css.indexOf("{", mediaStart);
if (openBrace === -1) {
break;
}
let depth = 0;
let closeBrace = -1;
for (let i = openBrace; i < css.length; i += 1) {
const ch = css[i];
if (ch === "{") {
depth += 1;
} else if (ch === "}") {
depth -= 1;
if (depth === 0) {
closeBrace = i;
break;
}
}
}
if (closeBrace === -1) {
throw new Error(`Unclosed @media block starting at index ${mediaStart}`);
}
blocks.push(css.slice(mediaStart, closeBrace + 1));
fromIndex = closeBrace + 1;
}
return blocks;
}
const mobileBlocks = extractMaxWidthMediaBlocks(768);
function findMobileBlockContaining(needle: string): string {
const block = mobileBlocks.find((candidate) => candidate.includes(needle));
expect(block, `Expected a 768px mobile block containing "${needle}"`).toBeTruthy();
return block ?? "";
}
describe("mission + planning modal mobile CSS", () => {
it("MissionManager: mission icon button touch targets are 44px", () => {
const missionBlock = findMobileBlockContaining(".mission-manager-overlay");
expect(missionBlock).toMatch(/\.mission-list__item-actions \.mission-icon-btn,[\s\S]*?\.mission-feature__actions \.mission-icon-btn\s*\{[\s\S]*?min-width:\s*44px;[\s\S]*?min-height:\s*44px;/s);
});
it("MissionManager: body prevents horizontal overflow", () => {
const missionBlock = findMobileBlockContaining(".mission-manager-overlay");
expect(missionBlock).toMatch(/\.mission-manager__body\s*\{[^}]*overflow-x:\s*hidden;/s);
});
it("MissionManager: feature actions wrap", () => {
const missionBlock = findMobileBlockContaining(".mission-manager-overlay");
expect(missionBlock).toMatch(/\.mission-feature__actions\s*\{[^}]*flex-wrap:\s*wrap;/s);
});
it("MissionManager: detail view includes safe-area bottom padding", () => {
const missionBlock = findMobileBlockContaining(".mission-manager-overlay");
expect(missionBlock).toMatch(/\.mission-detail\s*\{[^}]*env\(safe-area-inset-bottom/s);
});
it("SubtaskBreakdown: drag handle is touch-friendly (44px)", () => {
const planningBlock = findMobileBlockContaining(".planning-modal");
expect(planningBlock).toMatch(/\.subtask-drag-handle\s*\{[^}]*min-width:\s*44px;[^}]*min-height:\s*44px;/s);
});
it("SubtaskBreakdown: subtask action icon buttons are 44px", () => {
const planningBlock = findMobileBlockContaining(".planning-modal");
expect(planningBlock).toMatch(/\.subtask-item-actions \.btn-icon\s*\{[^}]*min-width:\s*44px;[^}]*min-height:\s*44px;/s);
});
it("SubtaskBreakdown: dependency chips are touch-friendly", () => {
const planningBlock = findMobileBlockContaining(".planning-modal");
expect(planningBlock).toMatch(/\.planning-dep-chip\s*\{[^}]*min-height:\s*44px;/s);
});
it("PlanningMode: confirm buttons meet touch targets", () => {
const planningBlock = findMobileBlockContaining(".planning-modal");
expect(planningBlock).toMatch(/\.planning-confirm-btn\s*\{[^}]*min-height:\s*44px;/s);
});
it("ModelSelection: combobox dropdown has mobile sizing", () => {
const modelBlock = findMobileBlockContaining(".model-combobox-dropdown");
expect(modelBlock).toMatch(/\.model-combobox-dropdown\s*\{[^}]*max-height:\s*50vh;[^}]*width:\s*min\(360px,\s*calc\(100vw - 32px\)\);/s);
});
it("ModelSelection: combobox search input is 16px on mobile", () => {
const modelBlock = findMobileBlockContaining(".model-combobox-search");
expect(modelBlock).toMatch(/\.model-combobox-search\s*\{[^}]*font-size:\s*16px;/s);
});
it("All planning modals: textareas are 16px to prevent iOS zoom", () => {
const planningBlock = findMobileBlockContaining(".planning-modal");
const hasPlanningTextarea = /\.planning-textarea\s*\{[^}]*font-size:\s*16px;/s.test(planningBlock);
const hasPlanningSummaryForm = /\.planning-summary-form[\s\S]*font-size:\s*16px;/s.test(planningBlock);
expect(hasPlanningTextarea || hasPlanningSummaryForm).toBe(true);
});
it("Planning modal: footer actions stack vertically on mobile", () => {
const planningBlock = findMobileBlockContaining(".planning-modal");
expect(planningBlock).toMatch(/\.planning-actions\s*\{[^}]*flex-direction:\s*column;/s);
});
it("ModelSelection: combobox options meet touch targets", () => {
const modelBlock = findMobileBlockContaining(".model-combobox-option");
expect(modelBlock).toMatch(/\.model-combobox-option\s*\{[^}]*min-height:\s*44px;/s);
});
});

View File

@@ -5935,6 +5935,20 @@ body {
width: 100%;
max-height: 200px;
}
/* Model combobox dropdown: mobile-friendly sizing */
.model-combobox-dropdown {
max-height: 50vh;
width: min(360px, calc(100vw - 32px));
}
.model-combobox-search {
font-size: 16px;
}
.model-combobox-option {
min-height: 44px;
}
}
/* === Tablet Responsive Tier (769px–1024px) === */
@@ -8911,15 +8925,25 @@ body {
/* Mobile responsive model dropdown */
@media (max-width: 768px) {
.model-combobox-dropdown {
max-height: 70vh;
max-height: 70dvh; /* Use dynamic viewport height where supported */
max-height: 50vh;
max-height: 50dvh; /* Use dynamic viewport height where supported */
width: min(360px, calc(100vw - 32px));
}
.model-combobox-search {
font-size: 16px;
}
.model-combobox-option {
min-height: 44px;
}
}
@media (max-width: 640px) {
.model-combobox-dropdown {
max-height: 60vh;
max-height: 60dvh;
max-height: 50vh;
max-height: 50dvh;
width: min(360px, calc(100vw - 32px));
}
}
@@ -14140,6 +14164,74 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
.planning-textarea {
font-size: 16px;
}
/* Planning options compact on mobile */
.planning-option {
padding: 12px;
}
/* Subtask breakdown: touch-friendly drag handles with padding */
.subtask-drag-handle {
min-width: 44px;
min-height: 44px;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
}
/* Subtask action buttons meet 44px touch target */
.subtask-item-actions .btn-icon {
min-width: 44px;
min-height: 44px;
}
/* Subtask items: prevent overflow */
.subtask-item-header {
flex-wrap: wrap;
gap: var(--space-xs);
}
/* Dependency chips: touch-friendly */
.planning-dep-chip {
min-height: 44px;
}
/* Size select: prevent iOS zoom */
.planning-size-select {
font-size: 16px;
}
/* Progress bar compact on narrow screens */
.planning-progress-bar {
gap: var(--space-xs);
}
.planning-progress-text {
font-size: 11px;
}
/* Confirm buttons meet 44px touch target */
.planning-confirm-btn {
min-height: 44px;
padding: var(--space-sm) var(--space-md);
}
/* Model selection: rows don't overflow */
.planning-modal-body .inline-create-model-row {
gap: var(--space-sm);
}
.planning-modal-body .model-badge {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
/* Model selection: prevent iOS zoom on select elements */
.planning-modal-body select {
font-size: 16px;
}
}
/* ============================================================
@@ -20846,6 +20938,46 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
.mission-confirm-panel__content {
padding: var(--space-md);
}
/* Header wraps on narrow screens */
.mission-manager__header {
flex-wrap: wrap;
gap: var(--space-xs);
}
/* Fix touch targets to meet 44px minimum */
.mission-list__item-actions .mission-icon-btn,
.mission-milestone__actions .mission-icon-btn,
.mission-slice__actions .mission-icon-btn,
.mission-feature__actions .mission-icon-btn {
min-width: 44px;
min-height: 44px;
}
/* Prevent horizontal overflow in body */
.mission-manager__body {
overflow-x: hidden;
}
/* Detail view safe area bottom padding */
.mission-detail {
padding-bottom: calc(var(--space-lg) + env(safe-area-inset-bottom, 0px));
}
/* Feature actions wrap on narrow screens */
.mission-feature__actions {
flex-wrap: wrap;
}
/* Progress bars fit narrow screens */
.mission-list__item-progress-bar {
max-width: 100%;
}
/* Confirm panel inputs fill width */
.mission-confirm-panel__content input[type="text"] {
width: 100%;
}
}
/* ── Workflow Results ── */