|
@@ -0,0 +1,489 @@
|
|
|
|
|
+# 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/<id>?t=<trail>&hl=<line>`)
|
|
|
|
|
+- 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 <synthesizedBy>`)
|
|
|
|
|
+ as 10.5px bordered pills. Uncertain targets fold into a `<details>` ("+ 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
|
|
|
|
|
+ (`<strong>N</strong> 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; `<details>` "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/<path>`)
|
|
|
|
|
+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/<key>`)
|
|
|
|
|
+Header: "Flow" + a `<select>` of flows (`--paper-2`, `--rule-soft` border, 12.5px sans) + a 78ch note.
|
|
|
|
|
+Cards **380px** wide, `--rule-soft` border (`--ink` on hover, `--accent` when current), header grid `16px | 1fr` padding `10px 12px 6px`
|
|
|
|
|
+(name 600 13px mono, `file:line` 11px `--ink-3`), separator `--rule-faint`, source window `12px/19px` mono with line numbers
|
|
|
|
|
+(grid `40px | 1fr | 6px`), the call line tinted `--accent-soft` and the calling identifier as an accent link; ±3 lines around the call.
|
|
|
|
|
+Links between cards: **86px** wide; a 1px `--ink-3` line with a filled arrowhead (polygon `76,3 84,7 76,11` in a 86×14 box);
|
|
|
|
|
+label 11px mono `--ink-3` centred (`calls`, `line 2029`; `via callback · registered at file:line`); uncertain dasharray `2 3`;
|
|
|
|
|
+heuristic dasharray `5 3`. End cap: **240px**, dashed `--rule-soft` border, 12px text — "Where the graph stops" + the boundary
|
|
|
|
|
+(form, key, line) + uncertain continuations. In the real build the strip is a Svelte Flow canvas laid out left→right with the
|
|
|
|
|
+same card/link visuals.
|
|
|
|
|
+
|
|
|
|
|
+### 3.6 Map (`#/map`)
|
|
|
|
|
+Grid: canvas `minmax(600px,1fr)` | side panel **320px** (`--rule-soft` left border, 14px 16px padding).
|
|
|
|
|
+Nodes: rect `width = max(110, label.length × 7.3 + 28)`, **height 40**, `--paper` fill, 1px `--ink` stroke (2px + `--press` fill
|
|
|
|
|
+when hovered/selected; `--ink-4` when dimmed; test modules dashed `4 3` in `--ink-3`), label 13px mono at (10,17), count
|
|
|
|
|
+"N symbols · M files" 11px `--ink-3` at (10,32). Layers: vertical gap **74px**, horizontal gap **34px**, padding 44px; entry points at the
|
|
|
|
|
+top ("entry points" label), foundations at the bottom ("foundations — depend on nothing below"); faint layer lines `--rule-faint`.
|
|
|
|
|
+Layout: aggregate edges by module; break 2-cycles keeping the heavier direction; longest-path layering (a module sits one layer
|
|
|
|
|
+above everything it depends on); barycenter ordering, 3 sweeps; single-node layers centred; ports spread along each box
|
|
|
|
|
+(`x = left + width × (i+1)/(n+1)` over the node's sorted out/in edges) so bundles fan. Edges: cubic `M x0,y0 C x0,my x1,my x1,y1`
|
|
|
|
|
+(`my` = midpoint), `stroke-width = min(6, 1 + log2(count) × 0.7)`, `--ink` at opacity 0.28 (hot 0.95, dimmed 0.06); a 12px transparent
|
|
|
|
|
+hit path per edge; edges with count < 4 (< 6 when tests included) hidden until a touching module is selected; cycle back-edges only when
|
|
|
|
|
+selected, `--accent` opacity 0.6, dasharray `4 3`. Tooltip: `--paper`, 1px `--ink` border, 8px 10px, 12px: "src/a → src/b", "N edges",
|
|
|
|
|
+by kind, top 4 symbol pairs. Side panel: title, 2-sentence explanation, hidden-edge note, "Include tests, scripts, kernel & site" checkbox,
|
|
|
|
|
+"Mutual dependencies" fold, selected module's dependencies/dependents with counts and its files. Fit: SVG width 100%,
|
|
|
|
|
+`viewBox` to content, `height: max(100%, 0.9 × content)` so labels never scale below ~0.9. In the real build this is a Svelte Flow
|
|
|
|
|
+canvas (custom node + custom edge components; hidden handles as ports; pan/zoom/fitView) with the same geometry.
|
|
|
|
|
+
|
|
|
|
|
+### 3.7 Search palette
|
|
|
|
|
+Results panel under the input: 1px `--ink` border, max-height 420px; group headers 12px `--ink-3` (`Flow`, `Symbols & files`);
|
|
|
|
|
+rows grid `18px | 1fr | auto`, 6px 10px, `--rule-faint` separators, selected/hover `--press`; name 12.5px mono + signature 11.5px mono
|
|
|
|
|
+`--ink-3` + location 11px mono. Flow grammar: "how does X reach Y", "X -> Y", "X → Y".
|
|
|
|
|
+
|
|
|
|
|
+## 4. Libraries and versions
|
|
|
|
|
+- Svelte 5 (≥ 5.25) + Vite (workspace `ui/`), Svelte Flow `@xyflow/svelte` ^1.6 for the Map and Flow canvases only (custom nodes/edges,
|
|
|
|
|
+ hidden handles for port spreading, local selection state — the pattern in docker-app's `StackGraph.svelte`); `@dagrejs/dagre` only as a
|
|
|
|
|
+ fallback if crossing quality demands it (never ELK). Symbol view = DOM + one SVG overlay (`ResizeObserver` re-layout).
|
|
|
|
|
+- Shiki (JavaScript regex engine, lazy grammars, custom near-monochrome theme as in §2.2) server-side in `/api/source`; tree-sitter-derived
|
|
|
|
|
+ tokens replace it in phase 3.
|
|
|
|
|
+- No native modules; no runtime dependency for the UI itself; the CLI serves **`dist/viewer/`** over `node:http`, loopback only.
|
|
|
|
|
+ (Not `dist/ui/` — `src/ui/` is the engine's *terminal* ui and tsc already compiles it there; see `ui/README.md`.)
|
|
|
|
|
+
|
|
|
|
|
+## 5. Copy rules
|
|
|
|
|
+Sentence case; controls say what happens ("Read as flow", "Clear"); counts always visible next to folds; honesty phrases fixed:
|
|
|
|
|
+"No test reaches this within 3 caller hops", "Reached by tests · N files within 3 hops", "Uncertain · N name-only matches, confidence < 0.6",
|
|
|
|
|
+"outside the index", "Where the graph stops", "changed on disk after the last index sync".
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Appendix — prototype stylesheet (verbatim; measurements above are derived from it)
|
|
|
|
|
+
|
|
|
|
|
+```css
|
|
|
|
|
+/* ---------- tokens: paper/ink editorial, one oxblood accent ---------- */
|
|
|
|
|
+:root {
|
|
|
|
|
+ --paper: #f7f6f2; --paper-2: #f1efe8; --press: #e8e6dd; --press-2: #dedbd0;
|
|
|
|
|
+ --ink: #16150f; --ink-2: #56544a; --ink-3: #87847a; --ink-4: #b4b1a5;
|
|
|
|
|
+ --rule: #16150f; --rule-soft: #d6d3c8; --rule-faint: #e6e3d9;
|
|
|
|
|
+ --accent: #7a2230; --accent-ink: #5e1a25; --accent-soft: #f0e3e5; --accent-line: #d9b3b9;
|
|
|
|
|
+ --amber: #8a5a0b; --amber-soft: #f3e9d2;
|
|
|
|
|
+ --sans: 'Archivo', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif;
|
|
|
|
|
+ --mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
|
|
|
|
+ --code-size: 12.5px; --code-lh: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+@media (prefers-color-scheme: dark) {
|
|
|
|
|
+ :root:not([data-theme="light"]) {
|
|
|
|
|
+ --paper: #16150f; --paper-2: #1c1a14; --press: #23211a; --press-2: #2c2a22;
|
|
|
|
|
+ --ink: #f3f1ea; --ink-2: #b8b5a8; --ink-3: #87847a; --ink-4: #5d5b52;
|
|
|
|
|
+ --rule: #f3f1ea; --rule-soft: #34322a; --rule-faint: #26241d;
|
|
|
|
|
+ --accent: #d48b96; --accent-ink: #e5a5ae; --accent-soft: #33201f; --accent-line: #6b3a42;
|
|
|
|
|
+ --amber: #d9a94a; --amber-soft: #2e2716;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+:root[data-theme="dark"] {
|
|
|
|
|
+ --paper: #16150f; --paper-2: #1c1a14; --press: #23211a; --press-2: #2c2a22;
|
|
|
|
|
+ --ink: #f3f1ea; --ink-2: #b8b5a8; --ink-3: #87847a; --ink-4: #5d5b52;
|
|
|
|
|
+ --rule: #f3f1ea; --rule-soft: #34322a; --rule-faint: #26241d;
|
|
|
|
|
+ --accent: #d48b96; --accent-ink: #e5a5ae; --accent-soft: #33201f; --accent-line: #6b3a42;
|
|
|
|
|
+ --amber: #d9a94a; --amber-soft: #2e2716;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+html, body { height: 100%; }
|
|
|
|
|
+body { margin: 0; background: var(--paper); color: var(--ink); font-family: var(--sans); font-size: 13px; line-height: 1.45; -webkit-font-smoothing: antialiased; }
|
|
|
|
|
+* { box-sizing: border-box; border-radius: 0 !important; }
|
|
|
|
|
+a { color: inherit; text-decoration: none; }
|
|
|
|
|
+button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
|
|
|
|
|
+.mono { font-family: var(--mono); }
|
|
|
|
|
+.dim { color: var(--ink-3); }
|
|
|
|
|
+.hidden { display: none !important; }
|
|
|
|
|
+:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
|
|
|
+@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
|
|
|
|
|
+
|
|
|
|
|
+#app { height: 100vh; display: grid; grid-template-rows: 48px 34px 1fr; }
|
|
|
|
|
+
|
|
|
|
|
+/* ---------- top bar ---------- */
|
|
|
|
|
+.topbar { display: grid; grid-template-columns: auto auto 1fr auto; align-items: center; gap: 22px; padding: 0 18px; border-bottom: 1px solid var(--rule); background: var(--paper); position: relative; z-index: 30; }
|
|
|
|
|
+.brand { display: flex; align-items: baseline; gap: 8px; }
|
|
|
|
|
+.brand-mark { display: inline-block; width: 10px; height: 10px; border: 1.5px solid var(--ink); background: var(--paper); align-self: center; }
|
|
|
|
|
+.brand-name { font-weight: 600; letter-spacing: -0.01em; font-size: 14px; }
|
|
|
|
|
+.brand-sub { color: var(--ink-3); font-size: 12px; }
|
|
|
|
|
+.views { display: flex; gap: 2px; }
|
|
|
|
|
+.views a { padding: 5px 10px; color: var(--ink-2); border-bottom: 2px solid transparent; }
|
|
|
|
|
+.views a:hover { color: var(--ink); }
|
|
|
|
|
+.views a.active { color: var(--ink); border-bottom-color: var(--ink); }
|
|
|
|
|
+.search { position: relative; max-width: 720px; }
|
|
|
|
|
+#q { width: 100%; height: 30px; padding: 0 10px; border: 1px solid var(--rule-soft); background: var(--paper-2); color: var(--ink); font: 13px var(--sans); }
|
|
|
|
|
+#q:focus { border-color: var(--ink); outline: none; }
|
|
|
|
|
+#q::placeholder { color: var(--ink-3); }
|
|
|
|
|
+.q-results { position: absolute; top: 32px; left: 0; right: 0; background: var(--paper); border: 1px solid var(--ink); max-height: 420px; overflow: auto; z-index: 40; }
|
|
|
|
|
+.q-row { display: grid; grid-template-columns: 18px 1fr auto; gap: 10px; align-items: baseline; padding: 6px 10px; border-bottom: 1px solid var(--rule-faint); cursor: pointer; }
|
|
|
|
|
+.q-row:last-child { border-bottom: 0; }
|
|
|
|
|
+.q-row:hover, .q-row.sel { background: var(--press); }
|
|
|
|
|
+.q-row .nm { font-family: var(--mono); font-size: 12.5px; }
|
|
|
|
|
+.q-row .sig { color: var(--ink-3); font-family: var(--mono); font-size: 11.5px; margin-left: 6px; }
|
|
|
|
|
+.q-row .loc { color: var(--ink-3); font-family: var(--mono); font-size: 11px; white-space: nowrap; }
|
|
|
|
|
+.q-head { padding: 6px 10px 4px; color: var(--ink-3); font-size: 12px; border-bottom: 1px solid var(--rule-faint); }
|
|
|
|
|
+.project { color: var(--ink-2); font-size: 12px; white-space: nowrap; }
|
|
|
|
|
+
|
|
|
|
|
+/* kind glyph: hollow square variants, mono letter */
|
|
|
|
|
+.k { display: inline-flex; width: 16px; height: 16px; align-items: center; justify-content: center; border: 1px solid var(--ink-3); color: var(--ink-2); font: 500 9.5px var(--mono); flex: 0 0 auto; }
|
|
|
|
|
+.k.fn { border-style: solid; }
|
|
|
|
|
+.k.cls, .k.iface, .k.struct, .k.type { background: var(--press); }
|
|
|
|
|
+.k.file { border-style: dashed; }
|
|
|
|
|
+
|
|
|
|
|
+/* ---------- trail bar ---------- */
|
|
|
|
|
+.trailbar { display: flex; align-items: center; gap: 0; padding: 0 18px; border-bottom: 1px solid var(--rule-soft); background: var(--paper-2); overflow-x: auto; white-space: nowrap; font-family: var(--mono); font-size: 12px; }
|
|
|
|
|
+.trailbar .label { color: var(--ink-3); font-family: var(--sans); margin-right: 10px; }
|
|
|
|
|
+.hop { display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px; color: var(--ink-2); border: 1px solid transparent; }
|
|
|
|
|
+.hop:hover { color: var(--ink); background: var(--press); }
|
|
|
|
|
+.hop.cur { color: var(--accent); border-color: var(--accent-line); background: var(--paper); }
|
|
|
|
|
+.hop-arrow { color: var(--ink-3); padding: 0 2px; }
|
|
|
|
|
+.hop-arrow.up { color: var(--ink-2); }
|
|
|
|
|
+.trailbar .spacer { flex: 1; }
|
|
|
|
|
+.trailbar .tb-btn { font-family: var(--sans); color: var(--ink-2); padding: 4px 8px; border: 1px solid var(--rule-soft); margin-left: 8px; background: var(--paper); }
|
|
|
|
|
+.trailbar .tb-btn:hover { border-color: var(--ink); color: var(--ink); }
|
|
|
|
|
+.trailbar .empty { color: var(--ink-3); font-family: var(--sans); }
|
|
|
|
|
+
|
|
|
|
|
+/* ---------- main / focus layout ---------- */
|
|
|
|
|
+#main { min-height: 0; overflow: hidden; }
|
|
|
|
|
+.focus { display: grid; grid-template-columns: 300px minmax(520px, 1fr); height: 100%; min-height: 0; }
|
|
|
|
|
+.rail-left { border-right: 1px solid var(--rule-soft); overflow: auto; background: var(--paper); }
|
|
|
|
|
+.stage { position: relative; overflow: auto; }
|
|
|
|
|
+.stage-inner { position: relative; display: grid; grid-template-columns: minmax(480px, 1fr) 320px; min-height: 100%; }
|
|
|
|
|
+.center { padding: 18px 22px 40px 22px; min-width: 0; }
|
|
|
|
|
+.rail-right { position: relative; border-left: 1px solid var(--rule-faint); }
|
|
|
|
|
+.overlay { position: absolute; inset: 0; pointer-events: none; overflow: visible; }
|
|
|
|
|
+.overlay path { fill: none; stroke: var(--ink-4); stroke-width: 1; }
|
|
|
|
|
+.overlay path.hot { stroke: var(--accent); stroke-width: 1.5; }
|
|
|
|
|
+.overlay path.uncertain { stroke-dasharray: 2 3; }
|
|
|
|
|
+.overlay path.heur { stroke-dasharray: 6 3; stroke: var(--ink-3); }
|
|
|
|
|
+.overlay path.origin { stroke: var(--accent); }
|
|
|
|
|
+
|
|
|
|
|
+/* rail headings */
|
|
|
|
|
+.rail-h { display: flex; align-items: baseline; justify-content: space-between; padding: 12px 14px 8px; font-weight: 600; font-size: 13px; border-bottom: 1px solid var(--rule-soft); position: sticky; top: 0; background: var(--paper); z-index: 2; }
|
|
|
|
|
+.rail-h .n { color: var(--ink-3); font-weight: 400; }
|
|
|
|
|
+.rail-h .hint { color: var(--ink-3); font-weight: 400; font-size: 11.5px; }
|
|
|
|
|
+.filegroup { padding: 10px 14px 4px; }
|
|
|
|
|
+.filegroup .fpath { font: 11px var(--mono); color: var(--ink-3); margin-bottom: 4px; display: flex; justify-content: space-between; gap: 8px; }
|
|
|
|
|
+.filegroup .fpath b { color: var(--ink-2); font-weight: 500; }
|
|
|
|
|
+.filegroup .fpath a:hover { color: var(--ink); text-decoration: underline; }
|
|
|
|
|
+.row { display: grid; grid-template-columns: 16px 1fr; gap: 8px; align-items: start; padding: 5px 6px 5px 4px; margin: 0 -6px; cursor: pointer; border: 1px solid transparent; position: relative; }
|
|
|
|
|
+.row:hover { background: var(--press); }
|
|
|
|
|
+.row.sel { border-color: var(--ink); }
|
|
|
|
|
+.row.origin { background: var(--accent-soft); border-color: var(--accent-line); }
|
|
|
|
|
+.row .nm { font: 12.5px var(--mono); color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
|
+.row .meta { color: var(--ink-3); font-size: 11px; margin-top: 1px; display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: baseline; }
|
|
|
|
|
+.row .kindlbl { color: var(--ink-3); }
|
|
|
|
|
+.row .chip { font: 11px var(--mono); color: var(--ink-2); border: 1px solid var(--rule-soft); padding: 0 4px; background: var(--paper); }
|
|
|
|
|
+.row .chip:hover { border-color: var(--ink); color: var(--ink); }
|
|
|
|
|
+.row.uncertain .nm, .row.stub .nm { color: var(--ink-2); }
|
|
|
|
|
+.row.uncertain .nm { text-decoration: underline dotted var(--ink-4); text-underline-offset: 3px; }
|
|
|
|
|
+.row.stub { cursor: default; }
|
|
|
|
|
+.row.stub .nm::after { content: ' ·'; color: var(--ink-4); }
|
|
|
|
|
+.fold { padding: 8px 14px; }
|
|
|
|
|
+.fold > summary { cursor: pointer; color: var(--ink-2); font-size: 12px; list-style: none; display: flex; gap: 6px; align-items: baseline; }
|
|
|
|
|
+.fold > summary::before { content: '+'; font-family: var(--mono); color: var(--ink-3); width: 10px; }
|
|
|
|
|
+.fold[open] > summary::before { content: '−'; }
|
|
|
|
|
+.fold .body { padding: 6px 0 0 16px; color: var(--ink-2); font-size: 12px; }
|
|
|
|
|
+.fold .body .fp { font: 11px var(--mono); color: var(--ink-2); padding: 2px 0; }
|
|
|
|
|
+.note { padding: 8px 14px; color: var(--ink-3); font-size: 11.5px; line-height: 1.4; }
|
|
|
|
|
+
|
|
|
|
|
+/* ---------- focus card ---------- */
|
|
|
|
|
+.card-h { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; }
|
|
|
|
|
+.card-h h1 { margin: 0; font: 600 20px/1.2 var(--mono); letter-spacing: -0.01em; }
|
|
|
|
|
+.card-h .kindword { color: var(--ink-3); font-size: 12.5px; }
|
|
|
|
|
+.card-h .loc { font: 11.5px var(--mono); color: var(--ink-2); }
|
|
|
|
|
+.card-h .loc a:hover { text-decoration: underline; }
|
|
|
|
|
+.badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
|
|
|
|
+.badge { font-size: 11.5px; color: var(--ink-2); border: 1px solid var(--rule-soft); padding: 2px 7px; background: var(--paper); display: inline-flex; gap: 5px; align-items: center; }
|
|
|
|
|
+.badge.ok { border-color: var(--rule-soft); }
|
|
|
|
|
+.badge.warn { color: var(--amber); border-color: var(--amber); background: var(--amber-soft); }
|
|
|
|
|
+.badge.hub { border-color: var(--ink); }
|
|
|
|
|
+.badge .sw { width: 8px; height: 8px; border: 1px solid currentColor; display: inline-block; }
|
|
|
|
|
+.badge.warn .sw { background: currentColor; }
|
|
|
|
|
+.sig { margin-top: 10px; font: 12px var(--mono); color: var(--ink-2); white-space: pre-wrap; word-break: break-word; }
|
|
|
|
|
+.doc { margin-top: 8px; color: var(--ink-2); font-size: 12.5px; max-width: 70ch; white-space: pre-wrap; }
|
|
|
|
|
+.parents { margin-top: 6px; font: 11.5px var(--mono); color: var(--ink-3); }
|
|
|
|
|
+.parents a:hover { color: var(--ink); text-decoration: underline; }
|
|
|
|
|
+.rel { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 6px; align-items: baseline; font-size: 12px; color: var(--ink-3); }
|
|
|
|
|
+.rel .chip { font: 11.5px var(--mono); color: var(--ink-2); border: 1px solid var(--rule-soft); padding: 1px 6px; cursor: pointer; background: var(--paper); }
|
|
|
|
|
+.rel .chip:hover { border-color: var(--ink); color: var(--ink); }
|
|
|
|
|
+
|
|
|
|
|
+/* code */
|
|
|
|
|
+.code { margin-top: 16px; border-top: 1px solid var(--rule); padding-top: 6px; font: var(--code-size)/var(--code-lh) var(--mono); }
|
|
|
|
|
+.ln { display: grid; grid-template-columns: 44px 1fr 18px; align-items: stretch; position: relative; }
|
|
|
|
|
+.ln:hover { background: var(--paper-2); }
|
|
|
|
|
+.ln.hot { background: var(--accent-soft); }
|
|
|
|
|
+.ln .no { color: var(--ink-4); text-align: right; padding-right: 12px; user-select: none; font-size: 11px; }
|
|
|
|
|
+.ln .tx { white-space: pre; overflow-x: auto; scrollbar-width: none; }
|
|
|
|
|
+.ln .tx::-webkit-scrollbar { display: none; }
|
|
|
|
|
+.ln .port { position: relative; }
|
|
|
|
|
+.ln .port i { position: absolute; right: 4px; top: 7px; width: 6px; height: 6px; border: 1px solid var(--ink-3); background: var(--paper); }
|
|
|
|
|
+.ln .port i.sure { background: var(--ink-3); }
|
|
|
|
|
+.ln.hot .port i { border-color: var(--accent); background: var(--accent); }
|
|
|
|
|
+.gap { color: var(--ink-4); padding: 2px 0 2px 44px; font-size: 11px; border-top: 1px dashed var(--rule-soft); border-bottom: 1px dashed var(--rule-soft); margin: 2px 0; }
|
|
|
|
|
+.t-c { color: var(--ink-3); }
|
|
|
|
|
+.t-s { color: var(--ink-2); }
|
|
|
|
|
+.t-k { font-weight: 500; }
|
|
|
|
|
+.t-n { color: var(--ink-2); }
|
|
|
|
|
+.t-def { font-weight: 600; }
|
|
|
|
|
+.ref { color: var(--accent); cursor: pointer; text-decoration: underline; text-decoration-color: var(--accent-line); text-underline-offset: 3px; }
|
|
|
|
|
+.ref:hover, .ref.hot { text-decoration-color: var(--accent); background: var(--accent-soft); }
|
|
|
|
|
+.ref.uncertain { color: var(--ink-2); text-decoration-style: dotted; text-decoration-color: var(--ink-4); }
|
|
|
|
|
+.ref.stub { color: var(--ink-2); text-decoration-color: var(--rule-soft); cursor: default; }
|
|
|
|
|
+
|
|
|
|
|
+/* callee rail rows (absolutely positioned to lines) */
|
|
|
|
|
+.rail-right .rrow { position: absolute; left: 14px; right: 12px; height: 34px; display: grid; grid-template-columns: 16px 1fr; gap: 8px; align-items: center; padding: 0 6px; border: 1px solid transparent; cursor: pointer; }
|
|
|
|
|
+.rail-right .rrow:hover { background: var(--press); }
|
|
|
|
|
+.rail-right .rrow.sel { border-color: var(--ink); }
|
|
|
|
|
+.rail-right .rrow.hot { background: var(--accent-soft); border-color: var(--accent-line); }
|
|
|
|
|
+.rail-right .rrow.origin { background: var(--accent-soft); }
|
|
|
|
|
+.rail-right .rrow .nm { font: 12.5px var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
|
+.rail-right .rrow .meta { font-size: 11px; color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: flex; gap: 8px; }
|
|
|
|
|
+.rail-right .rrow.uncertain .nm { color: var(--ink-2); text-decoration: underline dotted var(--ink-4); text-underline-offset: 3px; }
|
|
|
|
|
+.rail-right .rrow.stub { cursor: default; }
|
|
|
|
|
+.rail-right .rrow.stub .nm { color: var(--ink-2); }
|
|
|
|
|
+.rail-right .rrow .tag { font-size: 10.5px; color: var(--ink-3); border: 1px solid var(--rule-soft); padding: 0 4px; }
|
|
|
|
|
+.rail-right .rfold { position: absolute; left: 14px; right: 12px; }
|
|
|
|
|
+.rail-right .rfold summary { cursor: pointer; color: var(--ink-2); font-size: 12px; list-style: none; padding: 6px; }
|
|
|
|
|
+.rail-right .rfold summary::before { content: '+ '; font-family: var(--mono); color: var(--ink-3); }
|
|
|
|
|
+.rail-right .rfold[open] summary::before { content: '− '; }
|
|
|
|
|
+.rail-right .rfold .body .rrow { position: static; height: auto; padding: 4px 6px; }
|
|
|
|
|
+.rail-right .rnote { position: absolute; left: 20px; right: 12px; color: var(--ink-3); font-size: 11.5px; line-height: 1.4; }
|
|
|
|
|
+.rail-right .rail-h { position: sticky; }
|
|
|
|
|
+
|
|
|
|
|
+/* blast radius */
|
|
|
|
|
+.blast { margin-top: 22px; border-top: 1px solid var(--rule); padding-top: 10px; }
|
|
|
|
|
+.blast .bh { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 14px; }
|
|
|
|
|
+.blast .bh b { font-weight: 600; }
|
|
|
|
|
+.blast .stat { font-size: 12.5px; color: var(--ink-2); }
|
|
|
|
|
+.blast .stat strong { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
|
|
|
+.blast .bar { height: 6px; background: var(--press); margin-top: 8px; position: relative; max-width: 420px; }
|
|
|
|
|
+.blast .bar i { position: absolute; left: 0; top: 0; bottom: 0; background: var(--ink-2); }
|
|
|
|
|
+.blast .bar i.direct { background: var(--ink); }
|
|
|
|
|
+.blast .legend { color: var(--ink-3); font-size: 11.5px; margin-top: 4px; }
|
|
|
|
|
+.blast details { margin-top: 8px; }
|
|
|
|
|
+.blast summary { cursor: pointer; color: var(--ink-2); font-size: 12px; list-style: none; }
|
|
|
|
|
+.blast summary::before { content: '+ '; font-family: var(--mono); color: var(--ink-3); }
|
|
|
|
|
+.blast details[open] summary::before { content: '− '; }
|
|
|
|
|
+
|
|
|
|
|
+/* members outline (class / interface / file) */
|
|
|
|
|
+.outline { margin-top: 14px; border-top: 1px solid var(--rule); }
|
|
|
|
|
+.orow { display: grid; grid-template-columns: 16px minmax(160px, auto) 1fr auto; gap: 10px; align-items: baseline; padding: 6px 4px; border-bottom: 1px solid var(--rule-faint); cursor: pointer; }
|
|
|
|
|
+.orow:hover { background: var(--press); }
|
|
|
|
|
+.orow .nm { font: 12.5px var(--mono); }
|
|
|
|
|
+.orow .sig { font: 11.5px var(--mono); color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
|
+.orow .cnt { font: 11px var(--mono); color: var(--ink-3); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
|
|
|
+.orow.nested { padding-left: 22px; }
|
|
|
|
|
+.orow.dimmed .nm { color: var(--ink-3); }
|
|
|
|
|
+.subh { margin: 18px 0 4px; font-weight: 600; font-size: 13px; display: flex; gap: 8px; align-items: baseline; }
|
|
|
|
|
+.subh .n { color: var(--ink-3); font-weight: 400; }
|
|
|
|
|
+
|
|
|
|
|
+/* ---------- file view ---------- */
|
|
|
|
|
+.fileview { display: grid; grid-template-columns: 300px minmax(480px, 1fr) 300px; height: 100%; }
|
|
|
|
|
+.fileview .rail-left, .fileview .rail-r2 { overflow: auto; }
|
|
|
|
|
+.fileview .rail-r2 { border-left: 1px solid var(--rule-soft); }
|
|
|
|
|
+.fileview .center { overflow: auto; }
|
|
|
|
|
+.filerow { display: block; padding: 5px 14px; font: 12px var(--mono); color: var(--ink-2); cursor: pointer; border-bottom: 1px solid var(--rule-faint); }
|
|
|
|
|
+.filerow:hover { background: var(--press); color: var(--ink); }
|
|
|
|
|
+.filerow.stubf { color: var(--ink-3); cursor: default; }
|
|
|
|
|
+
|
|
|
|
|
+/* ---------- flow view ---------- */
|
|
|
|
|
+.flow { height: 100%; overflow: auto; padding: 18px 22px; }
|
|
|
|
|
+.flow-h { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px 18px; margin-bottom: 14px; }
|
|
|
|
|
+.flow-h h2 { margin: 0; font-size: 16px; font-weight: 600; }
|
|
|
|
|
+.flow-h select { font: 12.5px var(--sans); border: 1px solid var(--rule-soft); background: var(--paper-2); color: var(--ink); padding: 4px 8px; }
|
|
|
|
|
+.strip { display: flex; align-items: flex-start; gap: 0; overflow-x: auto; padding-bottom: 18px; }
|
|
|
|
|
+.hopcard { flex: 0 0 380px; border: 1px solid var(--rule-soft); background: var(--paper); cursor: pointer; }
|
|
|
|
|
+.hopcard:hover { border-color: var(--ink); }
|
|
|
|
|
+.hopcard.cur { border-color: var(--accent); }
|
|
|
|
|
+.hopcard .hh { padding: 10px 12px 6px; border-bottom: 1px solid var(--rule-faint); display: grid; grid-template-columns: 16px 1fr; gap: 8px; align-items: start; }
|
|
|
|
|
+.hopcard .hh .nm { font: 600 13px var(--mono); }
|
|
|
|
|
+.hopcard .hh .loc { font: 11px var(--mono); color: var(--ink-3); }
|
|
|
|
|
+.hopcard .hh .stepno { color: var(--ink-3); font-size: 11px; font-family: var(--mono); }
|
|
|
|
|
+.hopcard .win { padding: 6px 0 8px; font: 12px/19px var(--mono); }
|
|
|
|
|
+.hopcard .win .ln { grid-template-columns: 40px 1fr 6px; }
|
|
|
|
|
+.hopcard .win .ln .no { font-size: 10.5px; }
|
|
|
|
|
+.hopcard .win .ln .tx { white-space: pre; overflow: hidden; text-overflow: ellipsis; }
|
|
|
|
|
+.hopcard .nosrc { padding: 10px 12px; color: var(--ink-3); font-size: 12px; }
|
|
|
|
|
+.hoplink { flex: 0 0 86px; display: flex; flex-direction: column; align-items: center; padding-top: 14px; color: var(--ink-3); font: 11px var(--mono); text-align: center; gap: 4px; }
|
|
|
|
|
+.hoplink svg { width: 86px; height: 14px; display: block; }
|
|
|
|
|
+.hoplink svg line { stroke: var(--ink-3); stroke-width: 1; }
|
|
|
|
|
+.hoplink svg polygon { fill: var(--ink-3); }
|
|
|
|
|
+.hoplink.uncertain svg line { stroke-dasharray: 2 3; }
|
|
|
|
|
+.hoplink.heur svg line { stroke-dasharray: 5 3; }
|
|
|
|
|
+.hoplink .lbl { max-width: 84px; line-height: 1.3; }
|
|
|
|
|
+.endcap { flex: 0 0 240px; border: 1px dashed var(--rule-soft); padding: 12px; color: var(--ink-2); font-size: 12px; line-height: 1.45; align-self: stretch; }
|
|
|
|
|
+.endcap b { color: var(--ink); font-weight: 600; }
|
|
|
|
|
+.flow-note { color: var(--ink-3); font-size: 12px; max-width: 78ch; line-height: 1.5; }
|
|
|
|
|
+
|
|
|
|
|
+/* ---------- map view ---------- */
|
|
|
|
|
+.mapview { display: grid; grid-template-columns: minmax(600px, 1fr) 320px; height: 100%; }
|
|
|
|
|
+.mapstage { position: relative; overflow: auto; }
|
|
|
|
|
+.mapstage svg { display: block; width: 100%; }
|
|
|
|
|
+.mapside details { margin: 4px 0 10px; }
|
|
|
|
|
+.mapside summary::-webkit-details-marker { display: none; }
|
|
|
|
|
+.mapside { border-left: 1px solid var(--rule-soft); overflow: auto; padding: 14px 16px; }
|
|
|
|
|
+.mapside h2 { margin: 0 0 6px; font-size: 15px; font-weight: 600; }
|
|
|
|
|
+.mapside p { margin: 0 0 10px; color: var(--ink-2); font-size: 12.5px; line-height: 1.5; max-width: 40ch; }
|
|
|
|
|
+.mapside .toggle { display: flex; gap: 8px; align-items: center; font-size: 12.5px; color: var(--ink-2); margin: 10px 0 14px; cursor: pointer; }
|
|
|
|
|
+.mapside .toggle input { margin: 0; accent-color: var(--ink); }
|
|
|
|
|
+.mapside .cyc { font: 11.5px var(--mono); color: var(--ink-2); padding: 3px 0; }
|
|
|
|
|
+.mapside .cyc b { color: var(--accent); font-weight: 500; }
|
|
|
|
|
+.mapside .modlist { margin-top: 8px; }
|
|
|
|
|
+.mapside .edgeinfo { margin-top: 12px; border-top: 1px solid var(--rule-soft); padding-top: 10px; }
|
|
|
|
|
+.mapside .edgeinfo .pair { font: 11.5px var(--mono); color: var(--ink-2); padding: 2px 0; display: flex; justify-content: space-between; gap: 10px; }
|
|
|
|
|
+.mapside .edgeinfo .pair b { color: var(--ink); font-weight: 500; }
|
|
|
|
|
+.mnode rect { fill: var(--paper); stroke: var(--ink); stroke-width: 1; }
|
|
|
|
|
+.mnode text { font: 13px var(--mono); fill: var(--ink); }
|
|
|
|
|
+.mnode .cnt { font-size: 11px; fill: var(--ink-3); }
|
|
|
|
|
+.mnode.test rect { stroke-dasharray: 4 3; stroke: var(--ink-3); }
|
|
|
|
|
+.mnode.test text { fill: var(--ink-2); }
|
|
|
|
|
+.mnode:hover rect, .mnode.sel rect { stroke-width: 2; fill: var(--press); }
|
|
|
|
|
+.mnode.dimmed rect { stroke: var(--ink-4); }
|
|
|
|
|
+.mnode.dimmed text { fill: var(--ink-4); }
|
|
|
|
|
+.medge { fill: none; stroke: var(--ink); stroke-opacity: 0.28; cursor: pointer; }
|
|
|
|
|
+.medge:hover, .medge.hot { stroke-opacity: 0.95; }
|
|
|
|
|
+.medge.dimmed { stroke-opacity: 0.06; }
|
|
|
|
|
+.medge.cycle { stroke: var(--accent); stroke-opacity: 0.6; }
|
|
|
|
|
+.medge-hit { fill: none; stroke: transparent; stroke-width: 12; cursor: pointer; }
|
|
|
|
|
+.layerlbl { font: 12px var(--sans); fill: var(--ink-3); }
|
|
|
|
|
+.layerline { stroke: var(--rule-faint); stroke-width: 1; }
|
|
|
|
|
+.tip { position: absolute; z-index: 20; background: var(--paper); border: 1px solid var(--ink); padding: 8px 10px; font-size: 12px; color: var(--ink); pointer-events: none; max-width: 320px; }
|
|
|
|
|
+.tip .mono { font-size: 11.5px; }
|
|
|
|
|
+.tip .row2 { display: flex; justify-content: space-between; gap: 12px; color: var(--ink-2); }
|
|
|
|
|
+
|
|
|
|
|
+/* ---------- misc ---------- */
|
|
|
|
|
+.toast { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%); background: var(--ink); color: var(--paper); padding: 8px 14px; font-size: 12.5px; z-index: 50; max-width: 70ch; }
|
|
|
|
|
+.kbd { font: 11px var(--mono); border: 1px solid var(--rule-soft); padding: 0 4px; color: var(--ink-2); background: var(--paper); }
|
|
|
|
|
+.emptystate { padding: 40px; color: var(--ink-2); max-width: 60ch; line-height: 1.5; }
|
|
|
|
|
+.emptystate h2 { margin: 0 0 8px; font-size: 16px; }
|
|
|
|
|
+@media (max-width: 1100px) { .focus { grid-template-columns: 240px 1fr; } .stage-inner { grid-template-columns: minmax(360px, 1fr) 260px; } .fileview { grid-template-columns: 220px 1fr 220px; } .mapview { grid-template-columns: 1fr 260px; } }
|
|
|
|
|
+```
|