diff --git a/docs/README.md b/docs/README.md
index 3841369d..ffe47edd 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -21,6 +21,7 @@ Welcome to the GraphDone documentation! This directory contains comprehensive gu
### [Developer Guides](./guides/)
- [Getting Started](./guides/getting-started.md) - Setup and first steps
- [Architecture Overview](./guides/architecture-overview.md) - System design and technical decisions
+- [Web / UI Architecture](./guides/web-ui-architecture.md) - View system, graph canvas, mobile shell, node inspector
- [Testing Guide](../tests/README.md) - **E2E testing with robust authentication system**
- [SQLite Deployment Modes](./guides/sqlite-deployment-modes.md) - Local dev vs Docker authentication storage
- [User Flows](./guides/user-flows.md) - How teams actually use GraphDone
diff --git a/docs/guides/architecture-overview.md b/docs/guides/architecture-overview.md
index 1bf6f774..65e87687 100644
--- a/docs/guides/architecture-overview.md
+++ b/docs/guides/architecture-overview.md
@@ -8,6 +8,11 @@ GraphDone is architected around three core principles:
2. **Real-Time First**: Changes propagate immediately to all participants
3. **Democratic Coordination**: Priority emerges from community validation, not top-down assignment
+> **Client / UI layer:** for the web app's structure (view system, graph canvas,
+> mobile shell, node inspector) see [web-ui-architecture.md](./web-ui-architecture.md).
+> Some sections below describe the forward-looking server/infra target, not all of
+> which is wired up today.
+
## Current Architecture (v0.3.1-alpha)
```mermaid
diff --git a/docs/guides/web-ui-architecture.md b/docs/guides/web-ui-architecture.md
new file mode 100644
index 00000000..ef275135
--- /dev/null
+++ b/docs/guides/web-ui-architecture.md
@@ -0,0 +1,54 @@
+# Web / UI Architecture
+
+How the client (`packages/web`, React 18 + Vite + Tailwind + D3) is structured.
+This is the companion to [architecture-overview.md](./architecture-overview.md),
+which covers the server / graph engine / data layer.
+
+```mermaid
+graph TD
+ App["App.tsx
GraphProvider + ViewModeProvider"]
+ App --> Layout["Layout
sidebar · header · MobileBottomNav"]
+ Layout --> WS["Workspace
graph selector · data queries"]
+ WS --> VM["ViewManager
renders the active view mode"]
+ WS --> Insp["NodeInspector (docked)
+ on-canvas expand peek"]
+
+ VM -->|list / cards| Card["CardView"]
+ VM -->|graph| SGV["SafeGraphVisualization
→ InteractiveGraphVisualization (D3)"]
+ VM -->|table · kanban · gantt
calendar · dashboard · activity| Other["other views"]
+
+ VMC["ViewModeContext
active mode + persistence"] -.-> VM
+ VMC -.-> Layout
+ Insp --> Modes["Card · Contents (lazy markdown) · Diagram (sub-graph)"]
+ Audit["mobile-audit tests (CI)
layout · contrast · dialogs"] -.->|gate every screen| VM
+```
+
+## Pieces
+
+- **App shell** — `App.tsx` wraps the tree in `GraphProvider` (current graph +
+ drill-in/ascend) and **`ViewModeContext`** (`contexts/ViewModeContext.tsx`), the
+ single source of truth for the active view, persisted to `localStorage`. `Layout`
+ draws the chrome.
+- **View system** — `ViewManager` renders one of 8 modes: `cards`, `graph`,
+ `table`, `kanban`, `gantt`, `calendar`, `dashboard`, `activity`. Phones default to
+ **`cards`** (a readable list); desktop defaults to `graph`.
+- **Graph** — `SafeGraphVisualization` error-boundary-wraps
+ `InteractiveGraphVisualization`, the D3 force-directed canvas (one-shot physics,
+ viewport culling, LOD by zoom — see `LOD_THRESHOLDS`).
+- **Node inspector** — a docked `NodeInspector` plus an on-canvas **expand-in-place**
+ peek, each with a **Card / Contents / Diagram** toggle readable at any zoom
+ (`NodeContentRenderer` lazy-loads markdown/Prism; `NodeSubgraphPreview` draws a
+ capped static sub-graph). In-canvas card titles have a zoom-decoupled
+ **legibility floor**.
+
+## Responsive tiers (boundary: Tailwind `md`, 768px)
+
+- **Phone (` {
try {
diff --git a/tests/diagnostics/node-expand-legibility.spec.ts b/tests/diagnostics/node-expand-legibility.spec.ts
index 0cae3874..5eff8775 100644
--- a/tests/diagnostics/node-expand-legibility.spec.ts
+++ b/tests/diagnostics/node-expand-legibility.spec.ts
@@ -111,9 +111,6 @@ test.describe('node expand-in-place + legibility floor @geometry', () => {
await expect(panel, 'peek stays anchored through zoom').toBeVisible();
await page.keyboard.press('Escape');
await expect(panel, 'Esc closes the peek').toBeHidden({ timeout: 5000 });
-
- // eslint-disable-next-line no-console
- console.log('[expand] ok — anchored Card/Contents/Diagram peek verified');
});
test('PR-4: title stays above the on-screen legibility floor when zoomed out', async ({ page }) => {
@@ -125,9 +122,7 @@ test.describe('node expand-in-place + legibility floor @geometry', () => {
// Zoom OUT into the band where the native (un-counter-scaled) title would be
// sub-readable (k < ~0.857) but the label is still on screen.
const k = await zoomOutInto(page, 0.45, 0.7);
- // eslint-disable-next-line no-console
- console.log('[legibility] zoomed to k=' + k.toFixed(3));
- expect(k, 'reached the counter-scale band (k < 0.857)').toBeLessThan(0.857);
+ expect(k, `reached the counter-scale band (k=${k.toFixed(3)} < 0.857)`).toBeLessThan(0.857);
const probe = await page.evaluate(() => {
const texts = [...document.querySelectorAll('.graph-container svg .node-title-text')] as SVGTextElement[];
@@ -151,9 +146,6 @@ test.describe('node expand-in-place + legibility floor @geometry', () => {
// (e.g. 14px * 0.5 = 7px).
expect(probe.screenHeight, `title on-screen height >= floor (${LEGIBLE_FLOOR_PX}px)`).toBeGreaterThanOrEqual(LEGIBLE_FLOOR_PX - 3);
// Zoomed into the band, the counter-scale should be actively boosting (> 1).
- expect(probe.groupScale, 'legibility counter-scale is engaged when zoomed out').toBeGreaterThan(1);
-
- // eslint-disable-next-line no-console
- console.log('[legibility] title screenHeight=' + Math.round(probe.screenHeight) + 'px, groupScale=' + probe.groupScale);
+ expect(probe.groupScale, `legibility counter-scale engaged when zoomed out (h=${Math.round(probe.screenHeight)}px, scale=${probe.groupScale})`).toBeGreaterThan(1);
});
});