Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/guides/architecture-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions docs/guides/web-ui-architecture.md
Original file line number Diff line number Diff line change
@@ -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<br/>GraphProvider + ViewModeProvider"]
App --> Layout["Layout<br/>sidebar · header · MobileBottomNav"]
Layout --> WS["Workspace<br/>graph selector · data queries"]
WS --> VM["ViewManager<br/>renders the active view mode"]
WS --> Insp["NodeInspector (docked)<br/>+ on-canvas expand peek"]

VM -->|list / cards| Card["CardView"]
VM -->|graph| SGV["SafeGraphVisualization<br/>→ InteractiveGraphVisualization (D3)"]
VM -->|table · kanban · gantt<br/>calendar · dashboard · activity| Other["other views"]

VMC["ViewModeContext<br/>active mode + persistence"] -.-> VM
VMC -.-> Layout
Insp --> Modes["Card · Contents (lazy markdown) · Diagram (sub-graph)"]
Audit["mobile-audit tests (CI)<br/>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 (`<md`)** — `MobileBottomNav` (List / Graph / More) is the primary nav;
slim chrome; the sidebar/desktop header are hidden.
- **Tablet & desktop (`≥md`)** — sidebar rail + full top view-strip + all filters.

## Quality gate

`tests/e2e/mobile-audit.spec.ts` + `mobile-dialogs.spec.ts` run the
`tests/helpers/mobileAudit.ts` auditors (sideways-scroll, squeezed labels,
low-contrast/invisible text, modals clipped under the nav) across **every** screen
at phone width, in CI right after the smoke gate.
20 changes: 6 additions & 14 deletions packages/web/src/components/InteractiveGraphVisualization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,13 @@ import { spawnCelebration } from '../lib/celebration';
import { buildNeighborhood } from '../lib/graphAdjacency';
import { UndoStack } from '../lib/undoStack';

// LOD thresholds for different zoom levels
// Level-of-detail zoom thresholds (single source of truth): below each scale the
// matching per-node detail is hidden, for legibility and paint cost. (Values are
// the ones the graph actually ran on — a stale shadowing duplicate was removed.)
const LOD_THRESHOLDS = {
VERY_FAR: 0.1,
FAR: 0.3,
MEDIUM: 0.6,
CLOSE: 1.0,
VERY_FAR: 0.3, // below: viewport-cull, basic shapes only
FAR: 0.5, // below: hide type text + edit/grow/expand/descend icons
CLOSE: 0.6, // below: hide descriptions, edge labels, sub-graph counts
};

// Above this node count a graph is "dense": the continuous living-graph effects
Expand Down Expand Up @@ -795,15 +796,6 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i
// Window will be controlled manually by user interactions
}, [selectedNodes.size, editingEdge]);

// Level of detail thresholds
const LOD_THRESHOLDS = {
VERY_FAR: 0.3, // Only show basic shapes
FAR: 0.5, // Add node icons
MEDIUM: 0.8, // Add node titles
CLOSE: 0.6, // Add edge labels (earlier)
VERY_CLOSE: 2.0 // Full detail
};

// Function to save node position to database
const saveNodePosition = useCallback(async (nodeId: string, x: number, y: number) => {
try {
Expand Down
12 changes: 2 additions & 10 deletions tests/diagnostics/node-expand-legibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand All @@ -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[];
Expand All @@ -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);
});
});
Loading