# codegraph ui — design specification Authoritative visual + interaction spec for the `codegraph ui` viewer (Kommandr epics CG-39 → CG-48 → CG-56; Pro layers in docker-app DOCKERAPP-10). Companion to the design proposal ("Reading the graph") and the interactive prototype; the prototype's stylesheet is appended verbatim at the end and is the source of truth for every measurement below. Screenshots: `CodeGraph/codegraph-web-prototype/screenshots/` (also attached to the Kommandr epics). Design proposal: https://claude.ai/code/artifact/58336c87-9780-4018-8c04-37fe53236e96 Prototype: https://claude.ai/code/artifact/304bffb6-72d6-49c7-8f3a-9e4f244909f8 Prototype sources: `CodeGraph/codegraph-web-prototype/` (`proto.css`, `proto.js`, `extract.mjs`, `build.mjs`) ## 1. Principles (non-negotiable) 1. One symbol at a time — no whole-graph picture, no node-link neighborhood graph (decided). 2. Code order is the coordinate system — layouts by source line or dependency layer; deterministic; never force-directed. 3. Edges grow out of the code — every call edge is drawn from the line that makes the call (gutter port → callee row at that height). 4. Direction is spatial — callers left, callees right, flows read left→right, map dependencies point down. 5. Collapse the tails, show the counts — hubs badge (fan-in ≥ 40), tests fold, confidence < 0.6 folds ("uncertain"), outside-index counts; nothing silently dropped. 6. Honesty in the pixels — confidence = line style; heuristic (synthesized) edges dashed + wiring site; boundaries announced; drift banners; "no test within 3 hops" badge. ## 2. Visual language The engine's paper/ink editorial system (`site/src/styles/theme.css`): flat, hairline rules, **square corners everywhere** (`border-radius: 0 !important` globally), no shadows, no gradients, sentence case, **no tiny all-caps tracked labels**, one oxblood accent used only for focus/selection/edges, one amber used only for the "untested" warning. Syntax highlighting is deliberately near-monochrome so the graph's edges are the only colour in the code. ### 2.1 Color tokens | token | light | dark | used for | |---|---|---|---| | `--paper` | `#f7f6f2` | `#16150f` | page/body background (always set explicitly) | | `--paper-2` | `#f1efe8` | `#1c1a14` | trail bar, inputs, hovered code line, figure grounds | | `--press` | `#e8e6dd` | `#23211a` | hover fills, inline code background, bars | | `--press-2` | `#dedbd0` | `#2c2a22` | reserved (pressed state) | | `--ink` | `#16150f` | `#f3f1ea` | primary text, node borders, major rules | | `--ink-2` | `#56544a` | `#b8b5a8` | secondary text, strings, callers' names when uncertain | | `--ink-3` | `#87847a` | `#87847a` | tertiary text, comments, glyph borders, edge labels | | `--ink-4` | `#b4b1a5` | `#5d5b52` | line numbers, resting connectors, dimmed map nodes | | `--rule` | `#16150f` | `#f3f1ea` | top bar bottom rule, code/blast section rules | | `--rule-soft` | `#d6d3c8` | `#34322a` | rail dividers, chips, card borders | | `--rule-faint` | `#e6e3d9` | `#26241d` | row separators, map layer lines | | `--accent` | `#7a2230` | `#d48b96` | oxblood: call-site links, current trail hop, hot connectors, selected map edges | | `--accent-ink` | `#5e1a25` | `#e5a5ae` | accent text on accent-soft | | `--accent-soft` | `#f0e3e5` | `#33201f` | tinted rows ("you came from here"), hot code lines | | `--accent-line` | `#d9b3b9` | `#6b3a42` | accent borders/underlines at rest | | `--amber` | `#8a5a0b` | `#d9a94a` | "No test reaches this within 3 caller hops" badge only | | `--amber-soft` | `#f3e9d2` | `#2e2716` | that badge's fill | Theme selection: define the light set on bare `:root`; redefine under `@media (prefers-color-scheme: dark)` guarded as `:root:not([data-theme="light"])`; redefine again under `:root[data-theme="dark"]`. Never define a colour only inside a media/`[data-theme]` block. `body { background: var(--paper); color: var(--ink) }`. ### 2.2 Type - UI: **Archivo** 400/500/600/700 (fallback `-apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif`). - Code, symbol names, file paths, chips, trail, map labels: **IBM Plex Mono** 400/500/600 (+ italic 400) (fallback `ui-monospace, 'SF Mono', Menlo, Consolas, monospace`). - Scale: body UI `13px/1.45`; code `12.5px/20px`; symbol title `600 20px/1.2` mono, letter-spacing −0.01em; section labels (`Called by`, `Calls`, `Blast radius`) `600 13px` sans; rail rows `12.5px` mono name + `11px` sans meta; chips `11px` mono; line numbers `11px` mono in `--ink-4`; badges `11.5px`; map node label `13px` mono, count `11px`; flow card name `600 13px` mono, window `12px/19px` mono; trail `12px` mono. Headings sentence case, `text-wrap: balance`. - Code token classes: comment `--ink-3`; string `--ink-2`; keyword weight 500 (same ink); number `--ink-2`; definition name on its own line weight 600; **call-site link** = `--accent`, underline `--accent-line`, offset 3px, hover/hot fill `--accent-soft`; uncertain link = `--ink-2`, dotted underline `--ink-4`; link to a symbol outside the index = `--ink-2`, underline `--rule-soft`, not clickable. ### 2.3 Kind glyphs 16×16 hollow square, 1px `--ink-3` border, letter in `500 9.5px` mono: `ƒ` function · `m` method · `C` class · `I` interface · `S` struct · `T` type alias · `E` enum · `e` enum member · `k` constant · `v` variable · `p` property/field · `≡` file (dashed border) · `R` route · `⟨⟩` component · `N` namespace · `M` module · `Tr` trait · `U` union · `P` protocol. Container/type kinds get a `--press` fill. ## 3. Layout and components ### 3.1 App shell - Grid rows: **top bar 48px** / **trail bar 34px** / main. Top bar: brand (10px hollow square mark + "CodeGraph" 600 14px + "ui" in `--ink-3`), view tabs (`Map · Symbol · Flow`, 5px 10px padding, active = 2px `--ink` bottom border), search input (30px tall, `--paper-2` fill, `--rule-soft` border → `--ink` on focus, max-width 720px), project stats in `--ink-2` 12px. Bottom rule of the top bar is `--rule` (1px); the trail bar's is `--rule-soft`. - Focus ring everywhere: `outline: 2px solid var(--accent); outline-offset: 1px`. `prefers-reduced-motion` disables transitions. ### 3.2 Symbol view (`#/s/?t=&hl=`) - Grid: **left rail 300px** | stage `minmax(520px, 1fr)`; inside the stage: **center `minmax(480px, 1fr)`** | **right rail 320px**. Left rail has its own scroll; center + right rail scroll together in the stage (so callee rows stay aligned to lines). ≤ 1100px: 240px | `minmax(360px,1fr)` | 260px. - Rail headers sticky, `12px 14px 8px` padding, 600 13px, count in `--ink-3`, hint text right-aligned `11.5px` (`← step up`, `step down →`). - **Center**: padding `18px 22px 40px`. Header row: glyph, name (h1), kind word (`--ink-3` 12.5px, "· async · static · private"), location `file:start–end · N lines` (11.5px mono, file is a link). "in ClassName" breadcrumb 11.5px mono `--ink-3`. Badges row (gap 6px): `exported` · `hub · N callers` (border `--ink`) · tests badge (`Reached by tests · N files within 3 hops`, hollow 8px swatch) or amber warning (filled swatch). Signature 12px mono `--ink-2`, docstring 12.5px `--ink-2` max 70ch, relations row of chips (`extends X`, `implemented by …`, `uses types …` — chips 11.5px mono, `--rule-soft` border, 1px 6px). - **Code block**: 1px `--rule` top border + 6px; each line is a grid `44px | 1fr | 18px` (line number right-aligned, 12px right padding; text `white-space: pre`; port cell). Hover line → `--paper-2`; hot/highlighted line → `--accent-soft`. **Port**: 6×6 square, 1px `--ink-3` border, positioned right 4px / top 7px; filled `--ink-3` when the line has a resolved (≥ 0.6) edge, hollow when only uncertain; accent fill+border when hot. Gap rows ("⋯ N lines without calls"): 11px `--ink-4`, dashed `--rule-soft` top/bottom, 2px margin, indented 44px. Long bodies: head 80 lines + ±4-line windows around every call site; bodies ≤ 260 lines shown whole; containers show the outline instead of a body > 80 lines. - **Right rail rows** (`.rrow`): absolutely positioned, `left 14px right 12px`, **height 34px**, grid `16px | 1fr` gap 8px, padding `0 6px`, 1px transparent border (→ `--ink` when keyboard-selected; `--accent-line` + `--accent-soft` when hot/origin). Desired y = center of first call-site line − 17px; place in line order with `y = max(desired, prevY + 34 + 6)`; the stage's min-height grows to fit. Name 12.5px mono (`×N` in `--ink-3` when called from N lines); meta 11px `--ink-3`: file (or "same file"), edge word (`creates`, `passes as value`), tags (`hub · N`, `outside index`, `via `) as 10.5px bordered pills. Uncertain targets fold into a `
` ("+ Uncertain · N name-only matches, confidence < 0.6") placed 8px below the last row; "+N more calls into symbols outside the index" note 11.5px. - **Connectors** (SVG overlay covering the stage content): one cubic Bézier per call line → row: `M x0,ly C cx,ly cx,ry x1,ry` with `x0 = center right edge − 10`, `x1 = rail left + 14`, `cx = (x0+x1)/2`. Resting: `--ink-4` 1px; hot: `--accent` 1.5px; uncertain: dasharray `2 3`; heuristic: dasharray `6 3` in `--ink-3`; origin (the edge you arrived by): `--accent`. Left rail draws no connectors (separate scroll container); the origin caller row is tinted instead. (Real build: consider converging left connectors into the header — open question.) - **Left rail**: file groups (`.filegroup` padding `10px 14px 4px`; path 11px mono `--ink-3`, count bold `--ink-2`; the focus's own file first as "same file"); rows grid `16px | 1fr`, padding `5px 6px 5px 4px`, name 12.5px mono, meta row with edge-kind label + call-site chips (`:4657`, 11px mono, `--rule-soft` border, 0 4px; click = open caller at that line). Folds: `Tests · N calls from M files` (lists files), `Uncertain · N`. Origin row: `--accent-soft` fill + `--accent-line` border + "you came from here". Empty state note 11.5px `--ink-3`. - **Blast radius strip**: 22px above, 1px `--rule` top border, 10px padding-top; "Blast radius" 600 + stats (`N direct dependents · within 3 hops · files · test files · routes`, tabular-nums); bar 6px tall, max-width 420px, `--press` track, light fill `--ink-2` = within-3 share, dark fill `--ink` = direct share, both scaled to the widest radius in the index; legend 11.5px; `
` "What would need re-checking if this changed" listing dependents by file. - **Members outline** (classes, interfaces, structs, enums, files): rows grid `16px | minmax(160px,auto) | 1fr | auto`, padding `6px 4px`, `--rule-faint` separators, name 12.5px mono, signature 11.5px mono `--ink-3` ellipsised, counts `← in → out` 11px mono tabular; nested members indented 22px; properties/enum members dimmed. - **Keyboard**: `/` or ⌘K search · ↑/↓ (or j/k) move in the active rail · ←/→ switch rail · Enter follow · Backspace or `[` back · `m` map · `f` flow · Esc back to Symbol view. Selection = 1px `--ink` border on the row, scrolled into view. ### 3.3 Trail bar 34px, `--paper-2`, mono 12px. `Trail` label in `--ink-3` sans; hops as buttons (glyph + name, padding 4px 8px) separated by `→` (stepped into a call) or `←` (stepped up to a caller) in `--ink-3`; current hop: `--accent` text, `--accent-line` border, `--paper` fill; hover `--press`. Right side: `Read as flow`, `Clear` (sans 4px 8px, `--rule-soft` border). Empty hint in `--ink-3`. ### 3.4 File view (`#/file/`) Grid **300px | minmax(480px,1fr) | 300px**: Imported by · outline (source order, nested, counts, `line` number right) · Imports. File rows 12px mono, 5px 14px padding, `--rule-faint` separators; files outside the index in `--ink-3`, not clickable. Header: file glyph, basename as h1, `lang · KB · N symbols · generated`, full path. ### 3.5 Flow strip (`#/flow/`) Header: "Flow" + a `