fix(dashboard): stack mobile Add comment above Refine/Proceed

Phone no longer pins Add comment under the action rail as a fixed bar. It
uses the same full-width in-flow footer row as tablet, above Refine and
Proceed. The composer stays fixed when open.
This commit is contained in:
gsxdsm
2026-07-23 17:43:27 -07:00
parent 73a57d9487
commit b15fd2b498
5 changed files with 35 additions and 67 deletions

View File

@@ -1799,9 +1799,10 @@ FNXC:PlanningComments 2026-07-23-17:05:
Desktop (≥1025px) keeps the in-document trigger under the plan markdown. Tablet and phone
(≤1024px) use the action-rail counterpart so selection comments are not lost under the plan fold.
FNXC:PlanningComments 2026-07-24-05:35:
Tablet (769–1024) places that rail control as a full-width row above Refine/Proceed. Phone
(≤768) still lifts it into a fixed bottom bar above the mobile nav.
FNXC:PlanningComments 2026-07-24-05:50:
Phone and tablet both place that rail control as a full-width row above Refine/Proceed. A
viewport-fixed bar above the mobile nav sat under the action buttons and required an awkward
reach past Proceed — match the tablet in-flow stack on phone too.
*/
.planning-comment-quote,
@@ -1913,42 +1914,15 @@ plan actions, and a token-sized bottom inset keep all three controls inline with
*/
@media (max-width: 768px) {
/*
FNXC:PlanningComments 2026-07-23-17:05:
On phone, pin the rail Add-comment control to the visual viewport above the mobile nav so a
touch selection never requires scrolling the plan document or footer. Tablet (769–1024) keeps
the in-flow full-width rail row from the 1024px block instead.
The plan-actions button rule forces width 100 percent. With position fixed that 100 percent is
the viewport width, and combined with left/right insets the control overflowed past the right
edge (measured 780px wide in a 768px viewport). Higher specificity + width auto lets left/right
define the used width so the bar stays fully on-screen.
FNXC:PlanningComments 2026-07-24-05:50:
Phone keeps the ≤1024 in-flow full-width Add-comment row above Refine/Proceed (no fixed bar
under the action rail). The composer still pins above the mobile nav so opening it after a mid-
document selection does not require scrolling to the plan foot.
*/
.planning-plan-actions .btn.planning-add-comment--mobile {
position: fixed;
left: var(--space-md);
right: var(--space-md);
bottom: calc(
var(--mobile-nav-height, 44px)
+ max(env(safe-area-inset-bottom, 0px), 12px)
+ var(--space-md)
);
z-index: var(--z-popover);
width: auto;
max-width: none;
box-shadow: var(--shadow-md);
}
.planning-comment-tray li {
grid-template-columns: minmax(0, 1fr) auto;
}
/*
FNXC:PlanningComments 2026-07-23-17:05:
The comment composer used to flow at the end of the plan markdown, so opening it after a mid-
document selection required scrolling to the document foot. Pin it to the visual viewport above
the mobile nav/safe-area (and above the selection trigger slot) so Cancel / Add comment stay
reachable without scrolling.
*/
.planning-comment-editor {
position: fixed;
left: var(--space-md);

View File

@@ -3154,16 +3154,12 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
FN-8533 keeps the selection-adjacent control on wide desktop, but compact shells need a
counterpart that cannot be lost under the document fold.
FNXC:PlanningComments 2026-07-23-17:05:
On ≤768px the rail trigger is position:fixed above the mobile nav so a selection never
requires scrolling.
FNXC:PlanningComments 2026-07-24-05:35:
On tablet (769–1024) the same rail control stays in the plan action footer as a full-width
row above Refine/Proceed. Document-level selectionchange still dismisses it when the
selection collapses. CSS shows exactly one of the two variants; only established
768px/1024px breakpoint literals are allowed here, while all other dimensions remain
design-token based.
FNXC:PlanningComments 2026-07-24-05:50:
On tablet and phone (≤1024) the rail control stays in the plan action footer as a
full-width row above Refine/Proceed so a selection never requires scrolling past the
action baseline. Document-level selectionchange still dismisses it when the selection
collapses. CSS shows exactly one of the two variants; only established 768px/1024px
breakpoint literals are allowed here, while all other dimensions remain design-token based.
*/}
{selectedPlanQuote && !isCommentEditorOpen && (
<button

View File

@@ -170,18 +170,16 @@ describe("PlanningModeModal CSS responsive action contract", () => {
const css = loadPlanningCss();
const compactCss = getMediaBlocks(css, "@media (max-width: 1024px)").join("\n");
const mobileCss = getMediaBlocks(css, MOBILE_ACTIONS_QUERY).join("\n");
const tabletRailTriggerRule = findRule(compactCss, ".planning-plan-actions .btn.planning-add-comment--mobile");
const mobileTriggerRule = findRule(mobileCss, ".planning-plan-actions .btn.planning-add-comment--mobile");
const railTriggerRule = findRule(compactCss, ".planning-plan-actions .btn.planning-add-comment--mobile");
const mobileEditorRule = findRule(mobileCss, ".planning-comment-editor");
expect(findRule(css, ".planning-add-comment--mobile")).toMatch(/display\s*:\s*none\s*;/);
expect(findRule(compactCss, ".planning-add-comment--document")).toMatch(/display\s*:\s*none\s*;/);
expect(tabletRailTriggerRule).toMatch(/display\s*:\s*flex\s*;/);
expect(tabletRailTriggerRule).toMatch(/grid-column\s*:\s*1\s*\/\s*-1\s*;/);
expect(tabletRailTriggerRule).toMatch(/margin-top\s*:\s*0\s*;/);
expect(mobileTriggerRule).toMatch(/position\s*:\s*fixed\s*;/);
expect(mobileTriggerRule).toMatch(/width\s*:\s*auto\s*;/);
expect(mobileTriggerRule).toMatch(/var\(--mobile-nav-height/);
expect(railTriggerRule).toMatch(/display\s*:\s*flex\s*;/);
expect(railTriggerRule).toMatch(/grid-column\s*:\s*1\s*\/\s*-1\s*;/);
expect(railTriggerRule).toMatch(/margin-top\s*:\s*0\s*;/);
// FNXC:PlanningComments 2026-07-24-05:50: phone no longer overrides the rail trigger to fixed.
expect(findRule(mobileCss, ".planning-plan-actions .btn.planning-add-comment--mobile")).toBeUndefined();
expect(mobileEditorRule).toMatch(/position\s*:\s*fixed\s*;/);
expect(mobileEditorRule).toMatch(/var\(--mobile-nav-height/);
});

View File

@@ -36,7 +36,7 @@ describe("PlanningModeModal sequential layout", () => {
expect(css).toMatch(/\.planning-add-comment--mobile\s*\{[^}]*display\s*:\s*none\s*;/);
expect(css).toMatch(/@media \(max-width: 1024px\)[\s\S]*?\.planning-add-comment--document\s*\{[^}]*display\s*:\s*none\s*;/);
expect(css).toMatch(/@media \(max-width: 1024px\)[\s\S]*?\.planning-plan-actions \.btn\.planning-add-comment--mobile\s*\{[^}]*display\s*:\s*flex\s*;[^}]*grid-column\s*:\s*1\s*\/\s*-1\s*;/);
expect(css).toMatch(/@media \(max-width: 768px\)[\s\S]*?\.planning-plan-actions \.btn\.planning-add-comment--mobile\s*\{[^}]*position\s*:\s*fixed\s*;[^}]*width\s*:\s*auto\s*;/);
expect(css).not.toMatch(/@media \(max-width: 768px\)[\s\S]*?\.planning-plan-actions \.btn\.planning-add-comment--mobile\s*\{[^}]*position\s*:\s*fixed\s*;/);
expect(css).toMatch(/@media \(max-width: 768px\)[\s\S]*?\.planning-comment-editor\s*\{[^}]*position\s*:\s*fixed\s*;/);
});
});

View File

@@ -236,9 +236,9 @@ describe.runIf(executablePath)("Planning Mode browser E2E", () => {
Start at the preceding tab stop, then send actual Tab keys. Programmatic focus alone would
incorrectly accept a tabIndex=-1 contextual-comment trigger as keyboard reachable.
FNXC:PlanningComments 2026-07-24-05:35:
Phone keeps the trigger position:fixed above the nav; tablet keeps it in the action rail as
a full-width row above Refine/Proceed. Both stay in the actions DOM for focus order.
FNXC:PlanningComments 2026-07-24-05:50:
Phone and tablet keep the trigger in the action rail as a full-width row above
Refine/Proceed (not a fixed bar under the actions).
*/
focusable[triggerIndex - 1]?.focus();
return {
@@ -274,11 +274,12 @@ describe.runIf(executablePath)("Planning Mode browser E2E", () => {
hiddenButtonsTabbable: 0,
});
if (inFooter) {
expect(placement.actionLabels).toEqual(expect.arrayContaining(["Refine", "Proceed with plan"]));
if (!positionFixed) {
// Tablet in-flow rail row: Add comment sits above Refine/Proceed in the same footer.
expect(placement.actionLabels).toEqual(expect.arrayContaining(["Add comment to selection"]));
}
// In-flow rail row: Add comment sits above Refine/Proceed in the same footer.
expect(placement.actionLabels).toEqual(expect.arrayContaining([
"Add comment to selection",
"Refine",
"Proceed with plan",
]));
} else {
expect(placement.actionLabels).not.toContain("Add comment to selection");
}
@@ -314,9 +315,9 @@ describe.runIf(executablePath)("Planning Mode browser E2E", () => {
expect(afterOpen).toMatchObject({
addCommentButtons: 0,
actionChildren: 0,
// Phone pins the composer; tablet/desktop keep the in-document editor.
editorPosition: positionFixed ? "fixed" : "static",
editorInsideViewport: positionFixed ? true : expect.any(Boolean),
// Phone pins the composer when open; tablet/desktop keep the in-document editor.
editorPosition: viewport.width <= 768 && inFooter ? "fixed" : "static",
editorInsideViewport: viewport.width <= 768 && inFooter ? true : expect.any(Boolean),
});
await page.evaluate(() => {
@@ -334,9 +335,8 @@ describe.runIf(executablePath)("Planning Mode browser E2E", () => {
it("places the sole contextual comment trigger by viewport in embedded and modal Planning", async () => {
for (const presentation of ["embedded", "modal"] as const) {
// Phone: fixed bar above nav.
await verifyContextualCommentPlacement({ width: 768, height: 900 }, { inFooter: true, positionFixed: true }, presentation);
// Tablet: full-width action-rail row above Refine/Proceed.
// Phone + tablet: full-width action-rail row above Refine/Proceed.
await verifyContextualCommentPlacement({ width: 768, height: 900 }, { inFooter: true, positionFixed: false }, presentation);
await verifyContextualCommentPlacement({ width: 769, height: 900 }, { inFooter: true, positionFixed: false }, presentation);
await verifyContextualCommentPlacement({ width: 1024, height: 900 }, { inFooter: true, positionFixed: false }, presentation);
// Desktop: document-adjacent trigger only.