From 9958861121c28d945244e7e4115f6ff26d32921e Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 27 Oct 2026 07:28:18 -0700 Subject: [PATCH] fix(v2): pod board fits a 390px phone (TASK-576) At 390px the board head was one nowrap row (back, subtitle, New task), which squeezed the pod title to under 1px. On phone the head now wraps: the title takes a full-width row, and back and New task share the row above it. The board also kept the feature page's 34px desktop gutter on phone, and feature pages had no bottom tab bar there. The board is full-width on phone. V2FeaturePage now mounts V2MobileTabs and shares one attention fetch with the rail, and the pane reserves the bar's 56px so a scrolling body stops above the bar. Measured headless at 390x844 and 1440x900 on a fixture pod with two tasks: title 0.06px -> 73px; board 322px -> 390px wide; bottom tabs absent -> present at y 788. The 1440 board is unchanged. The task detail sheet covers the tab bar as a modal (z 100 over nav z 40). Settings and BYO bodies end at the bar's top with no document overflow. Pinned in v2-layout-invariants.test.ts: phone head wrap, board body padding, route class, bar height and reserve, V2FeaturePage mount. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: wren Agent-Model: claude/claude-haiku-5-5/max --- frontend/src/v2/V2App.tsx | 1 + .../v2/__tests__/V2AgentBYOMemory.test.tsx | 7 +++- .../v2/__tests__/V2AgentBYOPodPicker.test.tsx | 7 +++- .../v2/__tests__/v2-layout-invariants.test.ts | 38 +++++++++++++++++++ frontend/src/v2/components/V2FeaturePage.tsx | 6 ++- frontend/src/v2/v2.css | 26 +++++++++++++ 6 files changed, 82 insertions(+), 3 deletions(-) diff --git a/frontend/src/v2/V2App.tsx b/frontend/src/v2/V2App.tsx index ba71c593f1..ff7e6423f9 100644 --- a/frontend/src/v2/V2App.tsx +++ b/frontend/src/v2/V2App.tsx @@ -246,6 +246,7 @@ const V2App: React.FC = () => { false, /* V2PodBoard owns its own header (pod name + back). */ false, + 'v2-feature--board', )} /> () => null); +jest.mock('../hooks/useV2PodAttention', () => ({ + ...jest.requireActual('../hooks/useV2PodAttention'), + useV2PodAttention: () => ({ items: [], count: null, countByPod: {}, refresh: async () => {} }), +})); jest.mock('axios', () => { const mock = { diff --git a/frontend/src/v2/__tests__/V2AgentBYOPodPicker.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOPodPicker.test.tsx index 1e2afb9908..808300a744 100644 --- a/frontend/src/v2/__tests__/V2AgentBYOPodPicker.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentBYOPodPicker.test.tsx @@ -14,8 +14,13 @@ import { MemoryRouter } from 'react-router-dom'; import V2AgentBYO from '../components/V2AgentBYO'; import { AuthContext } from '../../context/AuthContext'; import { POD_VISITS_KEY } from '../lib/podRecency'; -// This suite owns the BYO request sequence, not the rail's attention query. +// This suite owns the BYO request sequence, not the attention queries the chrome +// makes. Each one would otherwise consume a queued pod-list response. jest.mock('../components/V2NavRail', () => () => null); +jest.mock('../hooks/useV2PodAttention', () => ({ + ...jest.requireActual('../hooks/useV2PodAttention'), + useV2PodAttention: () => ({ items: [], count: null, countByPod: {}, refresh: async () => {} }), +})); jest.mock('axios', () => { const mock = { diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 04c129e763..4b98030de1 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -5066,3 +5066,41 @@ test('OpenUI cards keep the approved phone controls and table records', () => { expect(css).toContain('.v2-root .v2-msg.v2-ui-card { display:flex; flex-direction:column;'); expect(css).toContain('.ui.gone{border-style:dashed'); }); + +describe('v2 board on a phone (TASK-576)', () => { + test('the board head wraps so the title keeps a full-width row above the actions', () => { + const css = read('../v2.css'); + const phone = mediaBlockContaining(css, '.v2-board__head'); + expect(ruleBody(phone, '.v2-board__head')).toContain('flex-wrap: wrap'); + expect(ruleBody(phone, '.v2-board__title-wrap')).toContain('flex: 1 1 100%'); + expect(ruleBody(phone, '.v2-board__title-wrap')).toContain('order: 1'); + expect(ruleBody(phone, '.v2-board__new')).toContain('margin-left: auto'); + }); + + test('the board drops the feature body gutter on a phone', () => { + const css = read('../v2.css'); + const phone = mediaBlockContaining(css, '.v2-feature--board > .v2-feature__body'); + expect(ruleBody(phone, '.v2-feature--board > .v2-feature__body')).toMatch(/padding:\s*0;/); + }); + + test('the board route passes the class that opts it into that override', () => { + expect(read('../V2App.tsx')).toMatch( + /,\s*false,\s*\/\*[^*]*\*\/\s*false,\s*'v2-feature--board'/, + ); + }); + + test('feature panes reserve the tab bar height so the body stops above the fixed nav', () => { + const css = read('../v2.css'); + const phone = mediaBlockContaining(css, 'padding-bottom: 56px'); + expect(ruleBody(phone, '.v2-feature')).toContain('padding-bottom: 56px'); + expect(ruleBody(phone, '.v2-mobile-tabs')).toContain('height: 56px'); + }); + + test('feature pages mount the phone tab bar and share one attention count with the rail', () => { + const page = read('../components/V2FeaturePage.tsx'); + expect(page).toContain("import V2MobileTabs from './V2MobileTabs';"); + expect(page).toContain('const attention = useV2PodAttention();'); + expect(page).toContain(''); + expect(page).toContain(''); + }); +}); diff --git a/frontend/src/v2/components/V2FeaturePage.tsx b/frontend/src/v2/components/V2FeaturePage.tsx index ca1625e908..0b61a98712 100644 --- a/frontend/src/v2/components/V2FeaturePage.tsx +++ b/frontend/src/v2/components/V2FeaturePage.tsx @@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next'; import V2NavRail from './V2NavRail'; import V2PodsSidebar from './V2PodsSidebar'; import { V2EmbeddedProvider } from '../hooks/useV2Embedded'; +import { useV2PodAttention } from '../hooks/useV2PodAttention'; +import V2MobileTabs from './V2MobileTabs'; import V2PageHead from './V2PageHead'; interface V2FeaturePageProps { @@ -298,6 +300,7 @@ const V2FeaturePage: React.FC = ({ }) => { const navigate = useNavigate(); const { t } = useTranslation(); + const attention = useV2PodAttention(); useEffect(() => { try { @@ -325,7 +328,7 @@ const V2FeaturePage: React.FC = ({ return (
- + {showPodsSidebar && }
{showHeader && } @@ -339,6 +342,7 @@ const V2FeaturePage: React.FC = ({
+
); }; diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index 5e17832cf3..89f7588a15 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -9003,11 +9003,31 @@ body.modern-ui.v2-canvas { } @media (max-width: 760px) { + /* At 390 the head was one nowrap row (back, subtitle, New task), which squeezed + the title to under 1px. On a phone the title takes its own full-width row; + back and New task share the row above it. */ .v2-board__head { + flex-wrap: wrap; + row-gap: 8px; padding: 12px 14px; gap: 10px; } + .v2-board__title-wrap { + order: 1; + flex: 1 1 100%; + } + + .v2-board__new { + margin-left: auto; + } + + /* The board is the phone's full-width surface. Its own head and columns carry + the 14px inset, so the feature body does not add its 34px desktop gutter. */ + .v2-feature--board > .v2-feature__body { + padding: 0; + } + .v2-board__columns { padding: 12px 14px 20px; } @@ -12215,6 +12235,12 @@ body.modern-ui.v2-canvas { padding: 6px 8px 4px; } + /* Feature pages mount this same bar (V2FeaturePage). Reserve its height on + the pane so a scrolling body stops above the fixed bar, not under it. */ + .v2-feature { + padding-bottom: 56px; + } + .v2-root button.v2-mobile-tabs__item { display: flex; width: auto;