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:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 ── */
|
||||
|
||||
Reference in New Issue
Block a user