r210 Minimal Header Implementation Report

Status: ✅ All green · 27/27 unit tests · 47/47 Playwright smoke · 0 typecheck errors

Date: 2026-08-30

Branch: feat/id9-r210-minimal-header @ 419b339

Repo: avidtech6/vibecoder-standalone

Base: clean/baseline-plus-wdm @ af00bff (r209 P0 header dead-code cleanup)

TL;DR

Replaces the r202-era header chrome (Home + Project name + 5 surface pills + 5 eye icons + hamburger) with a minimal React header:

74/74 tests pass (27 unit + 47 Playwright). No iframe, no postMessage, no pact edits.

What the operator sees

Before (r202):

[O] vibecoder | [MAIN] [WORKING] [DRAFT] [PREVIEW] [ORIGIN] [👁][👁][👁][👁][👁]        [☰]

After (r210):

[FV • vibecoder]  [MAIN]                                                    [🖼] [☰]

The ☰ icon is now a deliberate Edge Panel icon (SVG, 3 horizontal lines in a square). The Gallery icon is a 2x2 grid (SVG). Both are 32×32px.

Files changed (419b339)

 src/host/header/MinimalHeader.tsx       | +280 (NEW)
 src/host/header/GalleryPanel.tsx        | +210 (NEW)
 src/host/header/__tests__/MinimalHeader.test.tsx | +340 (NEW, 27 assertions)
 scripts/smoke/r210-smoke.spec.cjs       | +445 (NEW, 14 steps, 47 assertions)
 scripts/smoke/screenshots/              | 4 PNGs (NEW)
 src/host/host-boot.js                   | +160 -155 (mountMinimalHeader replaces mountOSHeader + openGalleryPanel + static React imports)
 src/host/workspace/index.ts             | +4 -14 (removed attachEyeHandlers() call, kept API)
 package.json                            | +3 (jsdom, @types/jsdom, @types/node — devDeps for tests)
 package-lock.json                       | +605 (locked by npm install)

Net: +2047 lines / -156 lines. The +2047 is mostly tests, smoke, package-lock + dependencies.

The 4 r210 components

1. `MinimalHeader.tsx` (280 LoC)

A React 18 component that renders the 4-element header. Reads window.__fvcmsActiveSurface and window.__fvcmsWorkspaceId. Subscribes to the fvcms:active-surface-changed document event. Click handlers:

The component is stateless beyond useState for the active surface mirror. It does NOT use the r206 useStudio hook (the r210 header is chrome-level, not a mount point in the r206 contract).

2. `GalleryPanel.tsx` (210 LoC)

A React 18 component for the new PM2 Gallery panel. Reads window.__fvcmsGallery.listModules() and window.__fvcmsGallery.CATEGORIES (the host-boot.js exposes these). Groups modules by category and renders a responsive grid. When the gallery stub returns [] (current vibecoder-standalone build), shows a helpful empty state pointing to a789 (Unified Gallery Panel plan).

3. `mountMinimalHeader()` in `host-boot.js` (~110 LoC)

Replaces mountOSHeader(). Injects CSS (only panel-clearance + mobile @media rules), mounts a fresh #fvcms-os-header div, and uses createRoot from react-dom/client to mount the React MinimalHeader. Includes a text-only fallback if the React import fails (so the operator is never stranded).

4. `openGalleryPanel()` in `host-boot.js` (~40 LoC)

Wires the Gallery icon to a new PM2 panel. If the panel already exists (fvcms-gallery-panel), activates it. Otherwise, mounts a fresh panel that wraps GalleryPanel.tsx via createRoot. Position: { x: 120, y: 120, w: 720, h: 540 }.

The function is exposed on window.__fvcmsOpenGalleryPanel for the React MinimalHeader to call.

The 3 critical r210 details

1. Vite dual-React gotcha (caught by smoke)

The first iteration used await Promise.all([import('react-dom/client'), import('./header/MinimalHeader.tsx')]) inside mountMinimalHeader(). This caused Vite to bundle a SECOND copy of React → "Invalid hook call" errors when MinimalHeader called useState. The fix: static imports at the top of host-boot.js:

import React from 'react';
import { createRoot } from 'react-dom/client';
import { MinimalHeader } from './header/MinimalHeader.tsx';
import { GalleryPanel } from './header/GalleryPanel.tsx';

2. CSS safety net for r202-era IDs

The r202-era IDs (#fvcms-os-header-pill-main, etc.) and the .fvcms-surface-eye class still appear in panel-manager.css (canonical pact code, "no pact edits" per spec). To prevent these from being visually resurrected by HMR or 3rd-party extensions, the new mountMinimalHeader injects a CSS safety net:

#fvcms-os-header-pills,
#fvcms-os-header-pill-main,
#fvcms-os-header-pill-working,
#fvcms-os-header-pill-draft,
#fvcms-os-header-pill-preview,
#fvcms-os-header-pill-origin,
#fvcms-os-header-home,
#fvcms-os-header-hamburger,
.fvcms-surface-eye
{ display: none !important; }

This is NOT a pact edit — it's a JS string-concat in the host-boot.js (per Rule 14 "no patches to canonical code" — patches here are in the host boot, not in the canonical CSS).

3. WDM `attachEyeHandlers()` call removed, API preserved

The WDM (Workspace Desktop Manager) had a togglePillHidden() API and an attachEyeHandlers() function that wired eye icons to the 5 surface pills. The function still exists in workspace-manager.ts for API stability, but the call in workspace/index.ts:55-56 is removed per the operator spec.

togglePillHidden() is still callable (no-op without pills). The CSS rules in panel-manager.css (body[data-pills-hidden~="X"] #fvcms-os-header-pill-X) are no-ops without the pills.

Test results

| Suite | Result |

|------|--------|

| r210 unit tests (MinimalHeader.test.tsx) | 27/27 ✅ |

| r210 Playwright smoke (r210-smoke.spec.cjs) | 47/47 ✅ |

| r210 typecheck | 0 errors ✅ |

| r210 iframe count (after comment strip) | 0 ✅ |

| r210 postMessage count (after comment strip) | 0 ✅ |

| WDM attachEyeHandlers is NOT called | ✅ (no .fvcms-surface-eye in DOM) |

| Total | 74/74 + 0 lint + 0 iframe + 0 postMessage |

r210 invariants verified

| Invariant | Status |

|-----------|--------|

| No iframe | ✅ (0 in r210 source + 0 in mount tree) |

| No postMessage | ✅ (0 in r210 source + 0 in mount tree) |

| Window-level event bus unchanged | ✅ (fvcms:active-surface-changed still on document) |

| Surface targeting preserved | ✅ (window.__fvcmsSetActiveSurface still wired) |

| Edge Panel open path unchanged | ✅ (icon calls window.__fvcmsOpenEdgePanel) |

| WDM togglePillHidden still works | ✅ (function preserved, no pills to dim) |

| WDM attachEyeHandlers is not called | ✅ (call removed) |

| PM2 panel addPanel pattern | ✅ (Gallery panel uses same mgr.addPanel(...) pattern as r202 VSIL) |

| Static React imports (no Vite dual-React) | ✅ |

| Test for r206/r207/r208 compatibility | ✅ (window globals only, no r206 StudioContext coupling) |

What's next

1. Production deploy (operator call)

cd /workspace/vibecoder-standalone-r210
npm run build  # build the production bundle
scp -O -r dist/* root@185.249.73.178:/var/www/freshvibeapps/clients/vibecoder/

2. Cross-branch integration (operator call)

The r210 branch is based on clean/baseline-plus-wdm @ af00bff (r209). It does NOT include r206/r207/r208 work. To merge r210 with r206/r207/r208, two paths:

3. Edge icon vs hamburger visual distinction

The new Edge icon is a 3-line SVG in a square. The old hamburger was ☰. They look different. If the operator wants a different look (e.g., the r202 hamburger glyph), it's a 1-line change in MinimalHeader.tsx.

4. Open questions

1. Merge order: r208 (in feat/id9-r208-integration) is also pending production. Do we merge r208 first, then r210? Or fast-track r210 (header) ahead of r208 (integration)?

2. r210 + r208 merge conflict on host-boot.js: r208 added 2 LoC in host-boot.js:335 (Step 6.5 boot call) and replaced openVsilPanel in EDGE_PANEL_HOST. r210 also changes host-boot.js heavily. We need a merge plan.

3. Gallery panel — wired to real listModules() or empty stub? The r210 panel shows the empty state (current stub). Once avidtech6/fv-module-gallery is wired into the dev server (per a789), the panel will populate automatically. No r210 change needed.

4. WDM togglePillHidden — keep the API or delete? Per spec, the API stays. But if 3rd-party callers don't use it, we can remove in a future P1 cleanup.

Verification commands

To re-run the smoke locally:

cd /workspace/vibecoder-standalone-r210
npx tsx src/host/header/__tests__/MinimalHeader.test.tsx  # 27/27
npx vite --port 5273 --host 0.0.0.0 &
node scripts/smoke/r210-smoke.spec.cjs  # 47/47

Expected: r210 MinimalHeader.test.tsx: 27 passed, 0 failed + r210 Playwright smoke: 47 passed, 0 failed.