@sigloch/graph-view-edit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # graph-view-edit
2
+
3
+ Das **Viewer/Editor-Familienmitglied** der aise-Familie — rendert den governten SE-Graphen,
4
+ den `graphcode` headless bereitstellt. graphcode liefert Daten, graph-view-edit rendert.
5
+
6
+ **Status:** MS-1 + MS-2 (Foundation + Renderer) + MS-3 (Edit-Surface) + MS-4 (Operational
7
+ Readiness, bis auf CR-GVE-116) — React/Vite/Zustand-Shell, deklarative View-Registry mit
8
+ 11 Graph-Views + 16 Dokument-Views, SE-Dashboard als Sibling-Route, Edit-Mode mit gegatetem
9
+ Write-Pfad (CR-GVE-008).
10
+
11
+ ## Start
12
+
13
+ ```bash
14
+ npm install
15
+ npm run dev # Vite-Dev-Server, http://localhost:5173
16
+ npm run build # Produktions-Bundle
17
+ npm run preview # Bundle lokal servieren
18
+ ```
19
+
20
+ Liest `docs/graph/<member>.graph.json` **client-seitig** direkt (kein zweiter Kuzu-Handle,
21
+ governance §8) — jede View fetched die committed Datei; kein Backend-Prozess nötig für die
22
+ Graph-Views.
23
+
24
+ ## Routen
25
+
26
+ - **`/view`** — View-Registry-Switcher: Hierarchy, Structure, Flows, Action-Flow, State,
27
+ Matrix, Tabular, Impact-Map, radiales/layered Flow-Layout, Schaltplan/IC-View (elkjs) +
28
+ 16 Dokument-Views. Deklarativ registriert in `src/view-descriptors.mjs`
29
+ (`src/view-registry.mjs`: `resolveViewCommand({name, scope, params})`).
30
+ - **`/dashboard`** — SE-Dashboard (Readiness/Recommendations/Artifacts/Health), Sibling-Route,
31
+ nicht Teil der View-Registry (Entscheid CR-GVE-D12). Die graphcode-Berechnung läuft
32
+ server-seitig in `vite.config.js`s Plugin-Middleware unter `GET /api/dashboard` —
33
+ `@sigloch/graphcode`s Panel-Funktionen ziehen Node-only-Internals nach, die im
34
+ Browser-Bundle nicht auflösen (siehe `docs/graph/graph-view-edit.graph.json`, CR-GVE-117/120).
35
+
36
+ ## Edit-Mode (CR-GVE-008)
37
+
38
+ `?edit=1` an einer View-URL aktiviert die Edit-Surface (`src/edit-mode.mjs`); Default bleibt
39
+ strikt read-only — ohne den Query-Param wird `createEditSurface` gar nicht erst konstruiert.
40
+
41
+ **Write-Pfad — kein zweiter Kuzu-Owner:** graph-view-edit öffnet niemals selbst einen
42
+ Kuzu-Handle. `POST /api/mutate` (`vite.config.js`s `mutateApiPlugin`) forwarded
43
+ `{commands, baseVersion, consumerId}` über `@sigloch/graphcode`s `callHost` (CR-GC-241) an
44
+ `.graphcode/host.sock` — den lokalen Unix-Socket, den der gewählte `graphcode mcp`-Host
45
+ ohnehin schon offen hält (CR-GC-235). Der Aufruf läuft durch **dasselbe** `graph_mutate`, das
46
+ jede MCP-Session auch benutzt — kein zweiter Schreibpfad, kein eigenes Store-Handle. Fehlt der
47
+ Socket (kein `graphcode`-Prozess für dieses Repo aktiv), antwortet `/api/mutate` mit einem
48
+ klaren 503 statt einem stillen No-op.
49
+
50
+ ```
51
+ Browser (?edit=1) → createEditSurface → command-bridge.mjs
52
+ → POST /api/mutate (vite.config.js, node-seitig)
53
+ → callHost('.graphcode/host.sock', 'graph_mutate', …)
54
+ → derselbe Apply-Gate, den `graphcode mcp` bedient
55
+ ```
56
+
57
+ `src/ontology-adapter.mjs` übersetzt `SE_DESCRIPTOR` (graph-api-core-Form) in
58
+ `@sigloch/graph-edit-surface`s `OntologyDescriptor` (nodeTypes/edgeTypes/tracePatterns) — die
59
+ schema-abgeleitete Edit-Surface (`packages/graph-edit-surface/`) bleibt ontologie-agnostisch
60
+ und bietet nur strukturell legale Optionen an ("das Gate entscheidet, nie das Menü").
61
+
62
+ ## Architektur
63
+
64
+ - **Tech-Stack (verriegelt, CR-GVE-D11):** React + Vite + Zustand, Cytoscape (Graph-Render),
65
+ elkjs (V12-Routing), TanStack Table (Data-Grid), Playwright (E2E/visuell), Vitest
66
+ (Unit/Component/API), CSS Custom Properties als Design-Tokens.
67
+ - **Renderer-Kern** (`src/viewer/Renderer.jsx` + `Chrome.jsx`): ein gemeinsamer
68
+ Cytoscape-Mount + Chrome (Zoom/Pan/Fit/Center, Typ+Kanten-Filter, Suche, Tiefe,
69
+ WeightProvider, Breadcrumb-Drilldown, Animation) für alle Graph-Views; jede View liefert
70
+ nur `selector`/`transform`/`layoutName`.
71
+ - **WeightProvider** (`src/weight-provider.mjs`): austauschbare Knotengröße/Kantenbreite
72
+ (uniform/traces/hot-spots).
73
+ - **elkjs-Routing** (`src/elk-routing.mjs`): FUNC als IC-Bausteine, FLOW→Ports, orthogonales
74
+ Layered-Routing — Herkunft: Spike `spikes/sp05-icview-elk.mjs`.
75
+
76
+ ## Tests
77
+
78
+ ```bash
79
+ npm test # vitest: Unit/Component (jsdom) + Playwright-Visualtests (echter Browser)
80
+ ```
81
+
82
+ - `tests/*.test.mjs` — Component-/Unit-Tests (Vitest + Testing Library, jsdom).
83
+ - `tests/visual/*.spec.mjs` — CR-GVE-109: Overlap-Gate (Playwright gegen echten
84
+ `vite`-Dev-Server, Bounding-Box-Kollisionsprüfung der Node-Marker) + Kontrast-Gate
85
+ (WCAG-AA-Messung der Design-Token-Farbpaare, reine Berechnung).
86
+
87
+ ## Roadmap
88
+
89
+ - **MS-3 (Edit-Surface) — done:** Cytoscape-Edit-Surface (`packages/graph-edit-surface/`,
90
+ CR-ES-001..004), Command-Bridge/Gate-Feedback/Text-Edit/Draft-Status (CR-GVE-110..113),
91
+ Write-Pfad-Adoption (CR-GVE-008, s.o.).
92
+ - **MS-4 (Operational Readiness):** dynamische Config + Access-Control (CR-GVE-114, done),
93
+ NFR-Regression-Suite (CR-GVE-115, done — `tests/nfr/*.bench.mjs`/`*.smoke.mjs`).
94
+ Offen: **CR-GVE-116** (voller Playwright-E2E-Customer-Journey über den echten Edit-Mode).
95
+ - **CR-GVE-124** (Label-Overlap-Vermeidung) — getrackt, noch nicht file-scoped, blockiert
96
+ MS-4 nicht.
97
+
98
+ **Lock:** graphcode bleibt headless; ein Kuzu-Owner pro Repo — der Viewer öffnet **keinen**
99
+ zweiten DB-Handle. Der Edit-Mode-Schreibpfad geht über `.graphcode/host.sock`
100
+ (`@sigloch/graphcode`s CR-GC-241-Export), nicht über einen eigenen Handle.
package/bin/gve.mjs ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // gve CLI (CR-GVE-181): serve the prebuilt viewer+editor against any repo.
3
+ // npx @sigloch/graph-view-edit --repo <path> [--port <n>]
4
+ // Runs `vite preview` on the shipped dist/; the API plugins (config,
5
+ // dashboard, mutate, events — vite.config.js) already attach there via
6
+ // configurePreviewServer. GVE_REPO_ROOT must be set even for cwd: preview
7
+ // serves only dist/, so docs/graph/* comes from graphStaticPlugin.
8
+ import { existsSync } from 'node:fs';
9
+ import { dirname, join, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
13
+ const args = process.argv.slice(2);
14
+ const opt = (name) => {
15
+ const i = args.indexOf(name);
16
+ return i !== -1 ? args[i + 1] : undefined;
17
+ };
18
+
19
+ const repoRoot = resolve(opt('--repo') ?? process.cwd());
20
+ if (!existsSync(repoRoot)) {
21
+ console.error(`gve: repo not found: ${repoRoot}`);
22
+ process.exit(1);
23
+ }
24
+ if (!existsSync(join(pkgRoot, 'dist', 'index.html'))) {
25
+ console.error('gve: no dist/ in the package — run `npm run build` first (published packages ship it via prepack)');
26
+ process.exit(1);
27
+ }
28
+
29
+ process.env.GVE_REPO_ROOT = repoRoot;
30
+ // Pin the config lookup to the TARGET repo (loadConfig treats a missing file
31
+ // as schema defaults) — otherwise a config.json in the cwd/package dir would
32
+ // hijack a --repo run. An explicit GVE_CONFIG_PATH still wins.
33
+ if (!process.env.GVE_CONFIG_PATH) process.env.GVE_CONFIG_PATH = join(repoRoot, 'config.json');
34
+
35
+ const { preview } = await import('vite');
36
+ const port = opt('--port');
37
+ const server = await preview({
38
+ root: pkgRoot,
39
+ preview: port ? { port: Number(port), strictPort: true } : {},
40
+ });
41
+ server.printUrls();
@@ -0,0 +1 @@
1
+ .gve-ctx{position:absolute;z-index:30;min-width:220px;max-width:320px;max-height:60vh;overflow-y:auto;display:flex;flex-direction:column;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:var(--shadow-1);padding:var(--space-1);font-family:var(--font-ui);font-size:var(--text-sm)}.gve-ctx-head{padding:var(--space-1) var(--space-2);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text-dim);border-bottom:1px solid var(--color-border);margin-bottom:var(--space-1)}.gve-ctx-item{text-align:left;padding:var(--space-1) var(--space-2);background:none;border:none;border-radius:var(--radius-sm);color:var(--color-text);cursor:pointer}.gve-ctx-item:hover:not(:disabled){background:var(--color-accent-soft)}.gve-ctx-item:disabled{color:var(--color-text-dim);cursor:not-allowed}.gve-ctx-form{display:flex;flex-direction:column;gap:var(--space-1);padding:var(--space-2)}.gve-ctx-form label{font-size:var(--text-xs);color:var(--color-text-dim)}.gve-ctx-form input,.gve-ctx-form textarea{font-family:var(--font-ui);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:var(--space-1) var(--space-2)}.gve-ctx-empty{padding:var(--space-1) var(--space-2);color:var(--color-text-dim);font-size:var(--text-xs)}.gve-genesis{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:20;display:flex;flex-direction:column;gap:var(--space-2);min-width:240px;padding:var(--space-3);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:var(--shadow-1);font-family:var(--font-ui);font-size:var(--text-sm);color:var(--color-text);text-align:center}.gve-genesis-title{font-family:var(--font-mono);font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.04em;color:var(--color-text-dim)}.gve-genesis-hint{margin:0;color:var(--color-text-dim)}.gve-genesis-input{font-family:var(--font-ui);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:var(--space-1) var(--space-2)}.gve-genesis-create{padding:var(--space-1) var(--space-2);background:var(--color-accent-soft);border:1px solid var(--color-border);border-radius:var(--radius-sm);color:var(--color-text);cursor:pointer}.gve-genesis-create:disabled{color:var(--color-text-dim);cursor:not-allowed}.gve-doc{font-family:var(--font-ui);font-size:var(--text-base);line-height:1.55;color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);padding:var(--space-8) var(--space-6);margin:var(--space-3) auto;max-width:820px}.gve-doc h1{font-size:var(--text-xl);margin:0 0 var(--space-3)}.gve-doc h2{font-size:var(--text-lg);margin:var(--space-6) 0 var(--space-3);padding-bottom:var(--space-1);border-bottom:1px solid var(--color-border)}.gve-doc h3{font-size:var(--text-base);margin:var(--space-4) 0 var(--space-2)}.gve-doc h4,.gve-doc h5,.gve-doc h6{font-size:var(--text-sm);margin:var(--space-3) 0 var(--space-1)}.gve-doc h5 code,.gve-doc h6 code{color:var(--color-text-dim)}.gve-doc p{margin:0 0 var(--space-2)}.gve-doc code{font-family:var(--font-mono);font-size:.92em;background:var(--color-surface-2);border-radius:var(--radius-sm);padding:0 var(--space-1)}.gve-doc blockquote{margin:0 0 var(--space-2);padding-left:var(--space-3);border-left:2px solid var(--color-border);color:var(--color-text-dim);font-size:var(--text-sm)}.gve-doc hr{border:0;border-top:1px solid var(--color-border);margin:var(--space-4) 0}.gve-doc pre.md-code{font-family:var(--font-mono);font-size:var(--text-sm);background:var(--color-surface-2);border-radius:var(--radius-sm);padding:var(--space-2) var(--space-3);overflow-x:auto}.edit-pen{margin-left:var(--space-2);font-size:var(--text-xs);background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:var(--radius-sm);cursor:pointer}.edit-box{margin:var(--space-2) 0;padding:var(--space-3);background:var(--color-surface-2);border:1px solid var(--color-accent);border-radius:var(--radius-md)}.edit-box textarea{width:100%;box-sizing:border-box;font-family:var(--font-mono);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:var(--space-2)}.cmd-preview{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text-dim);background:var(--color-surface);border:1px dashed var(--color-border);border-radius:var(--radius-sm);padding:var(--space-2);overflow-x:auto;white-space:pre-wrap}.edit-actions{display:flex;gap:var(--space-2);align-items:center;margin-top:var(--space-2)}.edit-actions .btn-primary{background:var(--color-accent);color:var(--color-text-inverse);border:1px solid var(--color-accent);border-radius:var(--radius-sm);padding:var(--space-1) var(--space-3);cursor:pointer}.edit-actions .btn-primary:disabled{opacity:.5;cursor:not-allowed}[data-testid=edit-errors],[data-testid=formate-errors]{color:var(--color-error);font-size:var(--text-xs)}[data-testid=edit-verdict][data-tier=block]{color:var(--color-error)}[data-testid=edit-verdict]{font-size:var(--text-xs);color:var(--color-text-dim)}.gve-cards{flex:1;overflow:auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:var(--space-4);padding:var(--space-4);align-content:start}.gve-card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:var(--shadow-1);padding:var(--space-4)}.gve-card-wide{grid-column:1 / -1}.gve-card h3{font-size:var(--text-xs);font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--color-text-dim);margin-bottom:var(--space-3);display:flex;align-items:center;gap:var(--space-2)}.gve-card h4{font-size:var(--text-xs);font-weight:600;color:var(--color-text-dim);margin-bottom:var(--space-2);display:flex;align-items:center;gap:var(--space-2)}.gve-count{font-size:var(--text-xs);font-weight:600;color:var(--color-text-inverse);background:var(--color-accent);border-radius:999px;padding:0 var(--space-2)}.gve-kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:var(--space-3);margin-bottom:var(--space-4)}.gve-kpi-tile{display:flex;flex-direction:column;gap:var(--space-1);background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:var(--space-3)}.gve-kpi-value{font-size:var(--text-xl);font-weight:600;line-height:1.1}.gve-kpi-label{font-size:var(--text-xs);color:var(--color-text-dim)}.gve-kpi-tile[data-tone=ok] .gve-kpi-value{color:var(--color-ok)}.gve-kpi-tile[data-tone=warn] .gve-kpi-value{color:var(--color-warn)}.gve-kpi-tile[data-tone=error] .gve-kpi-value{color:var(--color-error)}.gve-gate-cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:var(--space-4)}.gve-gates{list-style:none;padding:0;display:flex;flex-direction:column;gap:var(--space-2)}.gve-gate{display:grid;grid-template-columns:auto 44px 1fr auto 44px;align-items:center;gap:var(--space-2);font-size:var(--text-sm)}.gve-light{width:10px;height:10px;border-radius:50%;flex-shrink:0}.gve-light[data-state=passed]{background:var(--color-ok)}.gve-light[data-state=open]{background:var(--color-warn)}.gve-gate-id{font-family:var(--font-mono);font-weight:600}.gve-gate-label{color:var(--color-text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gve-gate-track{grid-column:4;width:90px;height:12px;background:var(--color-surface-2);border-radius:999px;overflow:hidden}.gve-gate-fill{display:block;height:100%;border-radius:999px}.gve-gate-fill[data-state=passed]{background:var(--color-ok)}.gve-gate-fill[data-state=open]{background:var(--color-warn)}.gve-gate-pct{font-family:var(--font-mono);text-align:right}.gve-gate-blk{grid-column:1 / -1;font-size:var(--text-xs);color:var(--color-warn)}.gve-legend{margin-top:var(--space-3);font-size:var(--text-xs);color:var(--color-text-dim)}.gve-rec-group{margin-bottom:var(--space-4)}.gve-rec-group h4[data-severity=error] .gve-count{background:var(--color-error)}.gve-rec-group h4[data-severity=warning] .gve-count{background:var(--color-warn)}.gve-rec-group h4[data-severity=info] .gve-count{background:var(--color-accent)}.gve-rec{border:1px solid var(--color-border);border-left-width:3px;border-radius:var(--radius-sm);padding:var(--space-2) var(--space-3);margin-bottom:var(--space-2);background:var(--color-surface-2)}.gve-rec[data-severity=error]{border-left-color:var(--color-error)}.gve-rec[data-severity=warning]{border-left-color:var(--color-warn)}.gve-rec[data-severity=info]{border-left-color:var(--color-accent)}.gve-rec-head{display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-1)}.gve-rid{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text-dim)}.gve-rec-el{color:var(--color-accent)}.gve-sev{font-size:var(--text-xs);font-weight:600;text-transform:uppercase;border-radius:var(--radius-sm);padding:0 var(--space-2)}.gve-sev[data-severity=error]{color:var(--color-error)}.gve-sev[data-severity=warning]{color:var(--color-warn)}.gve-sev[data-severity=info]{color:var(--color-accent)}.gve-rec-msg{font-size:var(--text-sm)}.gve-hint{margin-top:var(--space-1);font-size:var(--text-xs);color:var(--color-text-dim)}.gve-empty{font-size:var(--text-sm);color:var(--color-text-dim)}.gve-rollup summary{cursor:pointer;font-size:var(--text-sm);color:var(--color-text-dim);padding:var(--space-1) 0;-webkit-user-select:none;user-select:none}.gve-rollup summary:hover{color:var(--color-text)}.gve-rollup[open] summary{margin-bottom:var(--space-2)}.gve-fresh-list{list-style:none;padding:0;display:flex;flex-direction:column;gap:var(--space-2)}.gve-fresh-2col{display:grid;grid-template-columns:1fr 1fr;column-gap:var(--space-4)}.gve-fresh-list li{display:flex;align-items:center;gap:var(--space-2);font-size:var(--text-sm)}.gve-fresh-label{flex:1}.gve-fresh-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.gve-fresh-dot[data-freshness=live]{background:var(--color-ok)}.gve-fresh-dot[data-freshness=stale]{background:var(--color-warn)}.gve-fresh-dot[data-freshness=absent]{background:var(--color-error)}.gve-pill{font-size:var(--text-xs);border:1px solid var(--color-border);border-radius:999px;padding:0 var(--space-2)}.gve-pill[data-freshness=live]{border-color:var(--color-ok);color:var(--color-ok)}.gve-pill[data-freshness=stale]{border-color:var(--color-warn);color:var(--color-warn)}.gve-pill[data-freshness=absent]{border-color:var(--color-error);color:var(--color-error)}.gve-health-status{margin-bottom:var(--space-3);text-transform:uppercase}.gve-health-status[data-state=ok]{color:var(--color-ok)}.gve-health-status[data-state=error]{color:var(--color-error)}.gve-vtable{width:100%;border-collapse:collapse;font-size:var(--text-sm)}.gve-vtable td{padding:var(--space-1) var(--space-2);border-bottom:1px solid var(--color-border)}.gve-vtable td:first-child{font-family:var(--font-mono);color:var(--color-text-dim);white-space:nowrap}a.gve-btn{text-decoration:none;display:inline-block}.gve-appbar .title{font-size:var(--text-base)}.gve .gve{height:auto;flex:1;min-height:0}.gve-chip .dash{width:12px;height:0;border-top:2px solid var(--color-text-dim)}.gve .gve-body{flex:1;min-height:0}.gve-canvas,.gve-explorer .exp-tree,.gve-details{overflow:auto}.gve-tree .tw{cursor:pointer;-webkit-user-select:none;user-select:none}.gve-tree .row.sel{box-shadow:inset 2px 0 0 var(--color-accent)}.gve-tree .row.dim{opacity:.45;cursor:default}.gve-tree .det-tw{margin-left:var(--space-2);cursor:pointer;-webkit-user-select:none;user-select:none;background:none;border:0;padding:0 var(--space-1);color:var(--color-text-dim);font-size:var(--text-sm)}.gve-tree .det-tw.open,.gve-tree .det-tw:hover{color:var(--color-accent)}.gve-tree .row-details{margin:var(--space-1) 0 var(--space-2) 28px;padding:var(--space-2);border-left:2px solid var(--color-border);display:flex;flex-direction:column;gap:var(--space-1)}.gve-tree .row-details .det-desc{margin:0 0 var(--space-1);color:var(--color-text-dim);font-size:var(--text-sm)}.gve-tree .det-group{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-1) var(--space-2)}.gve-tree .det-type{font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.04em;color:var(--color-accent);min-width:64px}.gve-tree .det-neighbor{display:inline-flex;align-items:center;gap:var(--space-1);font-size:var(--text-sm);padding:0 var(--space-2);border:1px solid var(--color-border);border-radius:var(--radius-sm)}.gve-tree .det-neighbor .kind{font-style:normal;color:var(--color-text-dim);font-size:var(--text-xs)}.gve-grid-scroll{overflow:auto;flex:1;min-height:0}.gve-grid{border-collapse:collapse;font-size:var(--text-sm)}.gve-grid th,.gve-grid td{border:1px solid var(--color-border);padding:var(--space-1) var(--space-2);text-align:left;white-space:nowrap}.gve-grid thead th{position:sticky;top:0;z-index:1;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--color-surface-alt, var(--color-surface));color:var(--color-text)}.gve-grid thead th.corner{left:0;z-index:2}.gve-grid td.rowhead,.gve-grid th.corner{font-weight:600}.gve-grid td.rowhead{position:sticky;left:0;background:var(--color-surface)}.gve-grid td.hit{background:var(--color-accent-soft);color:var(--color-accent);text-align:center;font-weight:600}.tabular-grid th,.tabular-grid td{max-width:20rem}.tabular-grid:not(.wrap) th,.tabular-grid:not(.wrap) td{overflow:hidden;text-overflow:ellipsis}.tabular-grid.wrap th,.tabular-grid.wrap td{white-space:normal;word-break:break-word;vertical-align:top}.gve-group.tabs .gve-chip.active{background:var(--color-accent-soft);color:var(--color-accent);border-color:var(--color-accent)}:root{--color-bg: #f6f7f9;--color-surface: #ffffff;--color-surface-2: #eef0f3;--color-border: #d7dbe0;--color-text: #1a1d21;--color-text-dim: #5c6470;--color-text-inverse: #ffffff;--color-accent: #2563eb;--color-accent-soft: #dbeafe;--color-ok: #16a34a;--color-warn: #d97706;--color-error: #dc2626;--color-hotspot: #e11d48;--color-type-1: #2563eb;--color-type-2: #7c3aed;--color-type-3: #0891b2;--color-type-4: #16a34a;--color-type-5: #d97706;--color-type-6: #64748b;--color-type-7: #db2777;--color-type-8: #0d9488;--color-type-func: #0d9488;--color-type-flow: #2dd4bf;--fchain-1: #8b5cf6;--fchain-2: #f59e0b;--fchain-3: #ec4899;--fchain-4: #0284c7;--fchain-5: #84cc16;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-6: 24px;--space-8: 32px;--text-xs: 11px;--text-sm: 12.5px;--text-base: 14px;--text-lg: 16px;--text-xl: 20px;--font-ui: -apple-system, "Segoe UI", Roboto, sans-serif;--font-mono: ui-monospace, "SF Mono", Menlo, monospace;--radius-sm: 4px;--radius-md: 8px;--shadow-1: 0 1px 3px rgba(0,0,0,.12)}@media (prefers-color-scheme: dark){:root{--color-bg: #14161a;--color-surface: #1e2126;--color-surface-2: #262a31;--color-border: #363c45;--color-text: #e8eaed;--color-text-dim: #9aa3af;--color-text-inverse: #14161a;--color-accent: #60a5fa;--color-accent-soft: #1e3a5f;--color-type-2: #8b5cf6;--shadow-1: 0 1px 3px rgba(0,0,0,.5)}}*{box-sizing:border-box;margin:0}.gve{font-family:var(--font-ui);font-size:var(--text-base);background:var(--color-bg);color:var(--color-text);display:flex;flex-direction:column;height:100vh;overflow:hidden}.gve-bar{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) var(--space-4);background:var(--color-surface);border-bottom:1px solid var(--color-border);flex-wrap:wrap}.gve-bar .title{font-size:var(--text-lg);font-weight:600;margin-right:var(--space-4)}.gve-bar .title small{color:var(--color-text-dim);font-weight:400;margin-left:var(--space-2)}.gve-group{display:flex;align-items:center;gap:var(--space-1);background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:var(--radius-md);padding:2px var(--space-2)}.gve-group label{font-size:var(--text-xs);color:var(--color-text-dim);margin-right:var(--space-1)}.gve-btn{font:inherit;font-size:var(--text-sm);color:var(--color-text);background:transparent;border:none;border-radius:var(--radius-sm);padding:var(--space-1) var(--space-2);cursor:pointer}.gve-btn:hover{background:var(--color-accent-soft)}.gve-btn.active{background:var(--color-accent);color:var(--color-text-inverse)}.gve-input{font:inherit;font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:var(--space-1) var(--space-2);width:160px}.gve-select{font:inherit;font-size:var(--text-sm);background:var(--color-surface);color:var(--color-text);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:var(--space-1)}.gve-chip{display:inline-flex;align-items:center;gap:4px;font-size:var(--text-xs);border:1px solid var(--color-border);border-radius:999px;padding:2px var(--space-2);cursor:pointer;background:var(--color-surface);color:var(--color-text)}.gve-chip .dot{width:8px;height:8px;border-radius:50%}.gve-chip.off{opacity:.35}.gve-crumbs{font-size:var(--text-sm);color:var(--color-text-dim)}.gve-crumbs a{color:var(--color-accent);text-decoration:none}.gve-body{flex:1;display:flex;min-height:0}.gve-canvas{flex:1;overflow:auto;position:relative;min-width:0}.gve-explorer{width:260px;flex:0 0 260px;display:flex;flex-direction:column;background:var(--color-surface);border-right:1px solid var(--color-border);overflow:hidden;resize:horizontal}.gve-explorer .exp-tabs{display:flex;border-bottom:1px solid var(--color-border)}.gve-explorer .exp-tab{flex:1;font:inherit;font-size:var(--text-sm);cursor:pointer;background:transparent;border:none;padding:var(--space-2);color:var(--color-text-dim);border-bottom:2px solid transparent}.gve-explorer .exp-tab.active{color:var(--color-text);border-bottom-color:var(--color-accent);font-weight:600}.gve-explorer .exp-tree{flex:1;overflow:auto;padding:var(--space-2) 0;font-size:var(--text-sm)}.gve-tree,.gve-tree ul{list-style:none;margin:0;padding:0}.gve-tree ul{margin-left:14px;border-left:1px solid var(--color-border)}.gve-tree li{position:relative}.gve-tree .row{display:flex;align-items:center;gap:6px;padding:2px var(--space-2);white-space:nowrap;cursor:pointer;border-radius:var(--radius-sm)}.gve-tree .row:hover,.gve-tree .row.sel{background:var(--color-accent-soft)}.gve-tree .tw{width:10px;color:var(--color-text-dim);font-size:10px;flex:none}.gve-tree .ic{width:8px;height:8px;border-radius:2px;flex:none}.gve-tree .ic.ell{border-radius:50%}.gve-tree .lbl{overflow:hidden;text-overflow:ellipsis}.gve-tree .kind{font-size:var(--text-xs);color:var(--color-text-dim)}.gve-details{width:240px;flex:0 0 240px;overflow:auto;background:var(--color-surface);border-left:1px solid var(--color-border);padding:var(--space-3);font-size:var(--text-sm)}.gve-details h4{font-size:var(--text-sm);margin-bottom:var(--space-2)}.gve-details dt{font-size:var(--text-xs);color:var(--color-text-dim);margin-top:var(--space-2)}.gve-details dd{margin:0}.gve-status{display:flex;gap:var(--space-6);align-items:center;padding:var(--space-1) var(--space-4);font-size:var(--text-xs);color:var(--color-text-dim);background:var(--color-surface);border-top:1px solid var(--color-border)}.gve-node rect,.gve-node ellipse{fill:var(--color-surface);stroke:var(--t, var(--color-border));stroke-width:1.5}.gve-node text{font-family:var(--font-ui);font-size:var(--text-sm);fill:var(--color-text)}.gve-node .pill{fill:var(--t)}.gve-node .pill-text{fill:var(--color-text-inverse);font-size:var(--text-xs);font-weight:600}.gve-node.hotspot rect,.gve-node.hotspot ellipse{stroke:var(--color-hotspot);stroke-width:3}.gve-edge{stroke:var(--color-text-dim);fill:none;opacity:.55}.gve-edge.emph{stroke:var(--color-accent);opacity:1}.gve-edge-label{font-size:var(--text-xs);fill:var(--color-text-dim)}.gve-grid{border-collapse:collapse;margin:var(--space-4);font-size:var(--text-sm)}.gve-grid th,.gve-grid td{border:1px solid var(--color-border);padding:var(--space-1) var(--space-2);text-align:left}.gve-grid th{background:var(--color-surface-2);position:sticky;top:0}.gve-grid td.hit{background:var(--color-accent-soft);text-align:center}.gve-grid td.miss{color:var(--color-error);text-align:center}.gve-toast{position:absolute;right:var(--space-4);bottom:var(--space-4);background:var(--color-surface);border:1px solid var(--color-border);border-left:4px solid var(--color-warn);border-radius:var(--radius-md);box-shadow:var(--shadow-1);padding:var(--space-3) var(--space-4);font-size:var(--text-sm);max-width:360px}