diff --git a/packages/web/src/__tests__/setup.ts b/packages/web/src/__tests__/setup.ts index f149f27ae..533b774d3 100644 --- a/packages/web/src/__tests__/setup.ts +++ b/packages/web/src/__tests__/setup.ts @@ -1 +1,19 @@ import "@testing-library/jest-dom/vitest"; + +// jsdom does not implement window.matchMedia. Provide a minimal stub so +// components that call useMediaQuery (e.g. Dashboard) work in unit tests. +// The stub always returns `false` (non-matching), which keeps tests in the +// desktop/non-mobile rendering path and avoids spurious re-renders. +Object.defineProperty(window, "matchMedia", { + writable: true, + value: (query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + }), +}); diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css index 3bf74e6ce..705c4474b 100644 --- a/packages/web/src/app/globals.css +++ b/packages/web/src/app/globals.css @@ -1818,6 +1818,93 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { /* Card glow effects removed for Linear-clean look */ +/* ── Mobile accordion board ─────────────────────────────────────────── */ + +.accordion-board { + display: flex; + flex-direction: column; + gap: 0; + margin-top: 8px; +} + +.accordion-section { + border: 1px solid var(--color-border-default); + background: var(--color-bg-surface); + overflow: hidden; + transition: background 0.15s ease; +} + +.accordion-section + .accordion-section { + margin-top: 6px; +} + +.accordion-header { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + min-height: 44px; + padding: 0 14px; + background: none; + border: none; + cursor: pointer; + text-align: left; + color: var(--color-text-primary); + transition: background 0.12s ease; +} + +.accordion-header:hover { + background: var(--color-bg-hover); +} + +.accordion-header__dot { + width: 8px; + height: 8px; + border-radius: 999px; + flex-shrink: 0; +} + +.accordion-header__label { + font-size: 14px; + font-weight: 600; + flex: 1; + color: var(--color-text-primary); +} + +.accordion-header__count { + font-size: 12px; + font-weight: 500; + min-width: 20px; + padding: 2px 6px; + background: color-mix(in srgb, var(--color-border-default) 60%, transparent); + border-radius: 999px; + text-align: center; + color: var(--color-text-secondary); + font-variant-numeric: tabular-nums; +} + +.accordion-header__chevron { + font-size: 10px; + color: var(--color-text-tertiary); + flex-shrink: 0; + transition: transform 0.25s ease; +} + +/* Accordion body: hidden when collapsed, visible when expanded */ +.accordion-body { + display: grid; + grid-template-rows: 1fr; + transition: grid-template-rows 0.25s ease; +} + +.accordion-section--collapsed .accordion-body { + grid-template-rows: 0fr; +} + +.accordion-section--collapsed .accordion-body > * { + overflow: hidden; +} + @media (max-width: 960px) { .dashboard-hero__content { padding: 12px 14px; @@ -1997,10 +2084,9 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { max-height: 400px; } + /* Hide the "Attention Board" heading + legend on mobile — accordion provides its own headers */ .board-section-head { - flex-direction: column; - align-items: flex-start; - gap: 8px; + display: none; } .board-section-head__legend { diff --git a/packages/web/src/components/AttentionZone.tsx b/packages/web/src/components/AttentionZone.tsx index 420b95744..764b40954 100644 --- a/packages/web/src/components/AttentionZone.tsx +++ b/packages/web/src/components/AttentionZone.tsx @@ -11,6 +11,10 @@ interface AttentionZoneProps { onKill?: (sessionId: string) => void; onMerge?: (prNumber: number) => void; onRestore?: (sessionId: string) => void; + /** Accordion mode: whether this section is collapsed (mobile only) */ + collapsed?: boolean; + /** Accordion mode: called when the header is tapped to toggle */ + onToggle?: () => void; } const zoneConfig: Record< @@ -56,6 +60,11 @@ const zoneConfig: Record< /** * Kanban column — always renders (even when empty) to preserve * the board shape. Cards scroll independently within each column. + * + * When `collapsed` and `onToggle` are provided the component renders + * in accordion mode (mobile): a 44 px tappable header only, with the + * card list hidden. Empty sections in accordion mode omit the dashed + * placeholder entirely — just the single-line header is shown. */ function AttentionZoneView({ level, @@ -64,8 +73,51 @@ function AttentionZoneView({ onKill, onMerge, onRestore, + collapsed, + onToggle, }: AttentionZoneProps) { const config = zoneConfig[level]; + const isAccordion = onToggle !== undefined; + + if (isAccordion) { + return ( +