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;