pmx-canvas 0.4.6 → 0.4.8

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/Readme.md +56 -33
  3. package/dist/canvas/global.css +101 -3
  4. package/dist/canvas/index.js +93 -89
  5. package/dist/types/cli/shared.d.ts +13 -1
  6. package/dist/types/client/canvas/CanvasNode.d.ts +11 -0
  7. package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
  8. package/dist/types/client/nodes/AxStepControls.d.ts +4 -0
  9. package/dist/types/client/nodes/ExtAppFrame.d.ts +32 -0
  10. package/dist/types/client/nodes/FileNode.d.ts +14 -0
  11. package/dist/types/client/state/canvas-store.d.ts +10 -0
  12. package/dist/types/client/state/intent-bridge.d.ts +9 -0
  13. package/dist/types/server/ax-flow-loop.d.ts +5 -0
  14. package/dist/types/server/ax-interaction.d.ts +21 -2
  15. package/dist/types/server/canvas-db.d.ts +14 -0
  16. package/dist/types/server/canvas-operations.d.ts +5 -0
  17. package/dist/types/server/canvas-state.d.ts +8 -0
  18. package/dist/types/server/canvas-validation.d.ts +15 -21
  19. package/dist/types/server/file-content.d.ts +46 -0
  20. package/dist/types/server/html-primitives.d.ts +13 -1
  21. package/dist/types/server/operations/ops/ax-work.d.ts +1 -0
  22. package/dist/types/server/server.d.ts +1 -0
  23. package/dist/types/server/web-artifacts.d.ts +14 -0
  24. package/dist/types/shared/ax-flow.d.ts +51 -0
  25. package/dist/types/shared/node-sizes.d.ts +33 -0
  26. package/docs/RELEASE.md +48 -0
  27. package/docs/cli.md +19 -2
  28. package/docs/environment.md +1 -0
  29. package/docs/http-api.md +17 -0
  30. package/docs/mcp.md +12 -0
  31. package/docs/node-types.md +45 -1
  32. package/docs/screenshot.png +0 -0
  33. package/package.json +2 -2
  34. package/skills/pmx-canvas/SKILL.md +74 -9
  35. package/skills/pmx-canvas/evals/evals.json +54 -3
  36. package/skills/pmx-canvas/references/full-reference.md +11 -6
  37. package/skills/pmx-canvas-testing/SKILL.md +40 -0
  38. package/src/cli/commands/history.ts +1 -1
  39. package/src/cli/commands/nodes.ts +2 -2
  40. package/src/cli/commands/query.ts +1 -1
  41. package/src/cli/commands/view.ts +1 -1
  42. package/src/cli/index.ts +6 -0
  43. package/src/cli/shared.ts +34 -5
  44. package/src/client/App.tsx +28 -15
  45. package/src/client/canvas/CanvasNode.tsx +54 -5
  46. package/src/client/canvas/EdgeLayer.tsx +44 -24
  47. package/src/client/canvas/ExpandedNodeOverlay.tsx +11 -0
  48. package/src/client/canvas/auto-fit.ts +10 -1
  49. package/src/client/canvas/use-pan-zoom.ts +16 -1
  50. package/src/client/nodes/AxStepControls.tsx +226 -0
  51. package/src/client/nodes/ExtAppFrame.tsx +79 -2
  52. package/src/client/nodes/FileNode.tsx +249 -51
  53. package/src/client/state/canvas-store.ts +19 -0
  54. package/src/client/state/intent-bridge.ts +32 -0
  55. package/src/client/state/sse-bridge.ts +4 -1
  56. package/src/client/theme/global.css +101 -3
  57. package/src/server/ax-flow-loop.ts +114 -0
  58. package/src/server/ax-interaction.ts +299 -4
  59. package/src/server/canvas-db.ts +138 -32
  60. package/src/server/canvas-operations.ts +44 -17
  61. package/src/server/canvas-state.ts +20 -10
  62. package/src/server/canvas-validation.ts +50 -41
  63. package/src/server/demo-state.json +4605 -523
  64. package/src/server/demo.ts +24 -0
  65. package/src/server/file-content.ts +152 -0
  66. package/src/server/file-watcher.ts +7 -10
  67. package/src/server/html-primitives.ts +636 -0
  68. package/src/server/operations/ops/ax-work.ts +5 -0
  69. package/src/server/operations/ops/nodes.ts +15 -1
  70. package/src/server/operations/ops/viewport.ts +7 -1
  71. package/src/server/server.ts +106 -5
  72. package/src/server/web-artifacts.ts +109 -0
  73. package/src/shared/ax-flow.ts +102 -0
  74. package/src/shared/node-sizes.ts +52 -0
  75. package/dist/types/server/chart-template.d.ts +0 -29
  76. package/src/server/chart-template.ts +0 -448
package/CHANGELOG.md CHANGED
@@ -3,6 +3,82 @@
3
3
  All notable changes to `pmx-canvas` are documented here. This project follows
4
4
  [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.4.8] - 2026-08-20
7
+
8
+ ### Fixed
9
+
10
+ - Saving now writes only what actually changed instead of rewriting the whole
11
+ board on every save, so the write cost of dragging one node no longer grows
12
+ with board size ([#22](https://github.com/pskoett/pmx-canvas/issues/22)).
13
+ - Saves are now fsynced (`synchronous=FULL`), so an abrupt host crash no longer
14
+ loses the most recent saves — affordable now that saves are incremental.
15
+ - Orphaned large-content blobs are removed on shutdown — only when nothing
16
+ else has the canvas open, so a second live process can never lose content —
17
+ and `canvas.db` no longer grows without bound as node content is edited.
18
+ - MCP-app tiles now recover from black tiles in embedded hosts that never
19
+ report the page as visible (e.g. the GitHub Copilot panel), and recovery
20
+ logs no longer claim an unverified paint as a success there.
21
+
22
+ ## [0.4.7] - 2026-08-09
23
+
24
+ ### Added
25
+
26
+ - The flow works without the panel too: materialized step nodes get native
27
+ Start / Done / Blocked controls, and the first step adds Run loop / Stop and
28
+ a steer box. That loop runs server-side, so it survives a browser reload and
29
+ keeps advancing while the tab is closed.
30
+ - Two live AX control surfaces you can put on the board: **`ax-board`** (give
31
+ the agent tasks, watch their status change as it works, steer it, and run a
32
+ bounded task loop) and **`ax-flow`** (the same controls drawn as a task flow
33
+ with a loop-back rail, plus a **Materialize to board** button that lays the
34
+ flow out as real nodes joined by edges, each tracking its own status).
35
+ - The demo board is a proper showcase now: every node type, every chart type,
36
+ every HTML primitive, a grouped cluster, labelled edges and context pins —
37
+ laid out in labelled bands instead of the old three-group OKR dashboard.
38
+ - `GET /api/canvas/file-bytes?nodeId=…` serves a file node's bytes from the
39
+ workspace (used by the inline PDF viewer); paths outside the workspace, or
40
+ reached through a symlink out of it, are refused.
41
+ - File nodes now handle more than source code: `.csv` and `.tsv` files render
42
+ as real tables, PDFs render in an embedded viewer, and other binary files
43
+ show a name-and-size placeholder instead of decoded gibberish. Text files
44
+ larger than 2 MB render truncated with a notice rather than being read into
45
+ memory whole.
46
+
47
+ ### Changed
48
+
49
+ - The mouse wheel now zooms the canvas (trackpad two-finger scrolling still
50
+ pans, and the toolbar +/- buttons and their keyboard shortcuts both zoom
51
+ around the viewport centre).
52
+ - The browser honors the server's readability floor, so content-fit never
53
+ shrinks a node back below it.
54
+ - `validate` now reports edges whose endpoint node is docked (and therefore
55
+ cannot be drawn) as an advisory `hiddenEdgeEndpoints` list. Like
56
+ `sizeWarnings` it does not fail `ok`, so boards that passed before still
57
+ pass.
58
+
59
+ ### Fixed
60
+
61
+ - `strictSize` is no longer dropped on file, image and webpage nodes — it used
62
+ to be honored when sizing the node but never stored, so the browser resized
63
+ the node anyway.
64
+ - Node chrome no longer crowds out short nodes when you zoom out: the title bar
65
+ used to grow until it took most of a small node and clipped its text away.
66
+ - Context nodes no longer dock themselves invisibly: a node you create lands on
67
+ the canvas where you put it, and docking to the HUD column is something you
68
+ ask for. Previously an agent-created context node existed in the API and
69
+ passed validation but never rendered, so edges to it trailed off into empty
70
+ space.
71
+ - `fit` now sizes the board to the connected browser window instead of assuming
72
+ 1440x900, so fitting no longer leaves nodes clipped on both sides.
73
+ - Edges stay legible when you zoom out: strokes, dashes, arrowheads, and labels
74
+ now hold their on-screen size instead of shrinking to invisible hairlines,
75
+ and `references` edges use a higher-contrast colour.
76
+ - CLI flag values that begin with a dash now work, so a unified diff starting
77
+ with `--- a/file` can be passed straight to `--content` (and a bare `--` ends
78
+ flag parsing).
79
+ - Web-artifact builds now fail fast with a clear message naming the Node.js
80
+ version they need, instead of an opaque package-manager error.
81
+
6
82
  ## [0.4.6] - 2026-08-08
7
83
 
8
84
  ### Added
@@ -29,6 +105,7 @@ All notable changes to `pmx-canvas` are documented here. This project follows
29
105
  work items by status, and it refreshes automatically whenever work items
30
106
  change.
31
107
 
108
+
32
109
  ### Changed
33
110
 
34
111
  - **Nodes created with unreadably small explicit sizes are now clamped up to a
@@ -3044,6 +3121,8 @@ otherwise have to discover by trial and error.
3044
3121
  - Regression coverage for snapshot flat-`id` aliases on both MCP and
3045
3122
  HTTP surfaces, plus async / top-level-`await` WebView script bodies.
3046
3123
 
3124
+ [0.4.8]: https://github.com/pskoett/pmx-canvas/compare/v0.4.7...v0.4.8
3125
+ [0.4.7]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.4.7
3047
3126
  [0.4.6]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.4.6
3048
3127
  [0.4.5]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.4.5
3049
3128
  [0.4.4]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.4.4
package/Readme.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # pmx-canvas
2
2
 
3
3
  **A moldable canvas for agent-assisted thinking.** An infinite 2D surface
4
- where files, plans, status, charts, fetched web pages, annotations, and
5
- hand-drawn diagrams live side by side. Every node carries its own renderer; agents
4
+ where files, plans, status, charts, fetched web pages, annotations, code
5
+ diffs, and diagrams live side by side. Every node carries its own renderer; agents
6
6
  (and you) build new views in the middle of a session — even streaming a
7
7
  structured panel into place as they generate it — not as a separate tooling
8
8
  project. Pin what matters and the agent reads your spatial curation as
@@ -35,14 +35,21 @@ neighborhoods) and uses your layout to ground its next action.
35
35
 
36
36
  ### 02 / Mix any data source
37
37
 
38
- Files, web pages, screenshots, structured panels, charts, hand-drawn
39
- diagrams, embedded MCP Apps, and bundled web artifacts all live on the same
40
- surface. Any rich surface — an HTML node or a web artifact — can be **opened as
41
- a site**, full-page in its own browser tab with one click; the canvas and the
42
- tab render the same document. The reach of the canvas is the union of its
43
- [built-in node types](docs/node-types.md) and **whatever your agent's harness
44
- already has access to** — MCP servers, CLIs, file reads, web fetch, anything
45
- on its toolbelt.
38
+ Files, web pages, screenshots, structured panels, charts, diagrams, embedded
39
+ MCP Apps, and bundled web artifacts all live on the same surface. A file node
40
+ follows the file on disk as the agent edits it — source with line numbers, a
41
+ CSV or TSV as a real table, a PDF inline. Anything else binary shows its name
42
+ and size rather than decoded gibberish, and a very large text file renders
43
+ truncated with a note saying so. Code review has its own node: a unified
44
+ diff with per-file headers and colored added and removed lines. Diagrams can
45
+ be drawn by hand in Excalidraw or written as plain mermaid text and rendered
46
+ on the spot. Context cards, execution ledgers, and agent trace pills round out
47
+ the set. Any rich surface — HTML, mermaid, json-render, graph, webpage, or a
48
+ web artifact — can be **opened as a site**, full-page in its own browser tab
49
+ with one click; the canvas and the tab render the same document. The reach of
50
+ the canvas is the union of its [built-in node types](docs/node-types.md) and
51
+ **whatever your agent's harness already has access to** — MCP servers, CLIs,
52
+ file reads, web fetch, anything on its toolbelt.
46
53
 
47
54
  ### 03 / Annotate
48
55
 
@@ -61,13 +68,17 @@ immediately — an explicit, low-noise control over what the agent sees next.
61
68
  On top of pins, a host-agnostic **AX (agent-experience) layer** turns the
62
69
  canvas into a shared workspace between you and the agent:
63
70
 
64
- - **Agent-native nodes** — create markdown, status, HTML, json-render, graph,
65
- web-artifact, or MCP app nodes that can act as interactive controls for the
66
- agent. A node can focus context, create or update work, add evidence, request
67
- input or approval, or send steering without leaving the board.
71
+ - **Agent-native nodes** — most node types can act as interactive controls for
72
+ the agent out of the box: a node can focus context, create or update work,
73
+ add evidence, request input or approval, or send steering without leaving the
74
+ board. The sandboxed surfaces — HTML nodes and MCP apps — stay off until a
75
+ node opts in, since their content is author-supplied.
68
76
  - **Focus** — promote nodes into the agent's active context without moving the viewport.
69
77
  - **Work items & approval gates** — track visible tasks tied to nodes, and gate
70
- high-impact actions behind a human `pending → approved/rejected` decision.
78
+ high-impact actions behind a human `pending → approved/rejected` decision. A
79
+ linked node shows its work item's status on the title bar, and a live work
80
+ board lays the whole queue out by status and keeps itself current — useful
81
+ when several agents are working at once.
71
82
  - **Steering messages & agent-event timeline** — send instructions to the
72
83
  active session, and read a normalized, bounded timeline of prompts, tool
73
84
  runs, evidence (logs/diffs/screenshots/test-output), and failures.
@@ -109,8 +120,9 @@ Harness-agnostic. Drive the canvas from [MCP](docs/mcp.md) (22 tools,
109
120
  14 resources, change notifications), the [CLI](docs/cli.md), the
110
121
  [HTTP API](docs/http-api.md), or the [Bun SDK](docs/sdk.md) — all
111
122
  [environment variables documented here](docs/environment.md). Works with
112
- Claude Code, GitHub Copilot CLI, Codex, Cursor, Windsurf, or any agent
113
- that can spawn an MCP stdio server, call a CLI, or hit an HTTP endpoint.
123
+ Claude Code, the GitHub Copilot app and CLI, Codex, Amp, Cursor, Windsurf, or
124
+ any agent that can spawn an MCP stdio server, call a CLI, or hit an HTTP
125
+ endpoint.
114
126
 
115
127
  ### 07 / Native app adapters
116
128
 
@@ -203,15 +215,20 @@ To work on the canvas itself, clone the repo — see [Development](#development)
203
215
 
204
216
  ```bash
205
217
  bunx pmx-canvas # Start canvas, open browser
206
- bunx pmx-canvas --demo # Start with the saved dashboard demo board
218
+ bunx pmx-canvas --demo # Start with the showcase demo board
207
219
  bunx pmx-canvas --no-open # Headless (good for daemons / CI)
220
+ bunx pmx-canvas --theme=volt # Pick a theme (nine ship; PMX_CANVAS_THEME works too)
208
221
  bunx pmx-canvas --mcp # Run as MCP server (stdio)
209
222
  bunx pmx-canvas --help # All commands
210
223
  ```
211
224
 
225
+ Themes are a toolbar picker away too, and any one panel can override the
226
+ shared theme for itself with `?theme=<name|auto>`.
227
+
212
228
  The canvas opens at `http://localhost:4313`. Try `--demo` first — it seeds a
213
- saved dashboard with grouped OKR notes, graph panels, context pins, and labeled
214
- edges.
229
+ showcase board with every node type on it: notes and status, files, a diff, a
230
+ CSV table, charts of every kind, mermaid diagrams, the HTML primitives,
231
+ structured panels, a grouped cluster, labeled edges, and context pins.
215
232
 
216
233
  ### Connect your agent (MCP)
217
234
 
@@ -290,7 +307,7 @@ services:
290
307
  ```bash
291
308
  # .agents/setup — install the CLI (pin the exact version: a fresh orb running
292
309
  # @latest can silently pick up a newer release than the one you validated)
293
- npm install -g pmx-canvas@0.4.5
310
+ npm install -g pmx-canvas@0.4.8
294
311
  ```
295
312
 
296
313
  The server binds the portal-assigned `$PORT` automatically (gated on the
@@ -301,10 +318,11 @@ caveat: WebView automation (`pmx-canvas screenshot`) needs Bun >= 1.3.12 in
301
318
  the orb image; the canvas itself runs fine without it and says so in the
302
319
  error. The canvas detects the orb and the portal's nested-iframe
303
320
  embed on its own: live updates arrive over the proxy-safe polling transport,
304
- and iframe-backed nodes (HTML, graph, json-render, web artifacts) render
305
- inline via `srcdoc` with their theme styling included. Cross-origin hosted
306
- apps (e.g. the hosted Excalidraw MCP app) cannot be inlined and may stay
307
- blocked by the portal embed — everything served from the canvas itself works.
321
+ and iframe-backed nodes (HTML, mermaid, graph, json-render, web artifacts)
322
+ render inline via `srcdoc` with their theme styling included. Two things can't
323
+ be inlined and stay blocked by the portal embed: cross-origin hosted apps (e.g.
324
+ the hosted Excalidraw MCP app), and PDF file nodes — a PDF node offers an
325
+ "Open PDF" link there instead of a preview.
308
326
  Debug overrides: `/workbench?transport=poll|sse` and
309
327
  `?iframe-mode=srcdoc|src`. Once the service is up, `pmx-canvas smoke` verifies
310
328
  the whole stack (health, versions, MCP handshake, node lifecycle, validation)
@@ -332,7 +350,8 @@ Common harness skill directories: `.claude/skills/` (Claude Code),
332
350
  `.github/skills/` or `.copilot/skills/` (Copilot CLI),
333
351
  `.agents/skills/` (cross-harness convention). Once the canvas is running,
334
352
  the agent can read `canvas://skills` and pull in companion skills
335
- (`control-session-orchestrator`, `web-artifacts-builder`, `json-render-*`,
353
+ (`pmx-canvas-orchestration` for running several agents on one board,
354
+ `control-session-orchestrator`, `web-artifacts-builder`, `json-render-*`,
336
355
  `pmx-canvas-testing`, `playwright-cli`, etc.) as the work demands.
337
356
 
338
357
  ## Documentation
@@ -358,16 +377,20 @@ the agent can read `canvas://skills` and pull in companion skills
358
377
  `.pmx-canvas/canvas.db`.
359
378
  - **What leaves your machine.** The core canvas runs entirely on
360
379
  `localhost`. Network egress only happens for explicit, opt-in flows:
361
- `webpage` nodes fetch the URL you give them; `mcp-app` /
362
- `canvas_app { action: "diagram" }` calls go to whatever MCP server URL you
363
- configure (the Excalidraw preset uses `https://mcp.excalidraw.com/mcp`);
364
- `bunx` itself reads the npm registry on first install. Nothing else phones
365
- home.
380
+ `webpage` nodes and remote-URL `image` nodes fetch the address you give
381
+ them; `mcp-app` / `canvas_app { action: "diagram" }` calls go to whatever
382
+ MCP server URL you configure (the Excalidraw preset uses
383
+ `https://mcp.excalidraw.com/mcp`); building a web artifact runs a package
384
+ manager, which fetches a React/Tailwind toolchain plus that artifact's declared dependencies (and, if neither pnpm nor bun is present, installs pnpm globally) from the npm
385
+ registry; and `bunx` itself reads the registry on first install. Nothing
386
+ else phones home.
366
387
 
367
388
  ## Tech stack
368
389
 
369
390
  - **Runtime:** [Bun](https://bun.sh)
370
- - **UI:** [Preact](https://preactjs.com) + [@preact/signals](https://github.com/preactjs/signals)
391
+ - **UI:** [Preact](https://preactjs.com) + [@preact/signals](https://github.com/preactjs/signals);
392
+ the json-render/graph viewer bundle is React + [recharts](https://recharts.org),
393
+ and mermaid nodes render with [mermaid](https://mermaid.js.org)
371
394
  - **Styling:** CSS custom properties + Tailwind (json-render bundle only)
372
395
  - **Server:** Bun.serve (HTTP + SSE)
373
396
  - **MCP:** [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) (stdio)
@@ -380,7 +403,7 @@ cd pmx-canvas
380
403
  bun install
381
404
  bun run build
382
405
  bun run dev # Start + open browser
383
- bun run dev:demo # Start with the demo board
406
+ bun run dev:demo # Start with the showcase demo board
384
407
  bun run test # Unit tests
385
408
  bun run test:e2e # Playwright end-to-end tests
386
409
  bun run test:all # Unit tests + browser smoke
@@ -643,6 +643,10 @@ body,
643
643
  overflow: hidden;
644
644
  text-overflow: ellipsis;
645
645
  flex: 1;
646
+ /* The name is the one thing that must survive a cramped title bar. Everything
647
+ else in the bar is flex-shrink: 0, so without a floor the title absorbed all
648
+ the pressure and collapsed to an ellipsis on narrow nodes. */
649
+ min-width: 9ch;
646
650
  min-width: 0;
647
651
  }
648
652
 
@@ -670,6 +674,13 @@ body,
670
674
  }
671
675
 
672
676
  .canvas-node .node-ax-status {
677
+ /* A long status ("in-progress") must not push the node's NAME out of the bar;
678
+ the pill is a glance-level cue, the name is the identity. */
679
+ max-width: 11ch;
680
+ overflow: hidden;
681
+ text-overflow: ellipsis;
682
+ white-space: nowrap;
683
+ flex-shrink: 1;
673
684
  font-size: calc(9px * var(--node-chrome-scale, 1));
674
685
  padding: calc(1px * var(--node-chrome-scale, 1)) calc(6px * var(--node-chrome-scale, 1));
675
686
  border-radius: calc(4px * var(--node-chrome-scale, 1));
@@ -1846,10 +1857,11 @@ html.is-node-resizing .ext-app-preview-catcher {
1846
1857
  border-radius: 999px;
1847
1858
  }
1848
1859
 
1849
- /* Edge labels */
1860
+ /* Edge labels — --edge-chrome-scale is set on the edge <svg> so labels keep a
1861
+ constant screen size while zoomed out (see edgeChromeScale in EdgeLayer). */
1850
1862
  .edge-label {
1851
1863
  font-family: var(--font);
1852
- font-size: 11px;
1864
+ font-size: calc(11px * var(--edge-chrome-scale, 1));
1853
1865
  pointer-events: none;
1854
1866
  user-select: none;
1855
1867
  }
@@ -1857,7 +1869,7 @@ html.is-node-resizing .ext-app-preview-catcher {
1857
1869
  .edge-label-bg {
1858
1870
  fill: var(--c-panel-glass);
1859
1871
  stroke: var(--c-line);
1860
- stroke-width: 0.5;
1872
+ stroke-width: calc(0.5 * var(--edge-chrome-scale, 1));
1861
1873
  }
1862
1874
 
1863
1875
  /* Trace nodes — compact pills for agent tool/subagent visualization */
@@ -1963,6 +1975,92 @@ html.is-node-resizing .ext-app-preview-catcher {
1963
1975
  color: var(--c-text-soft);
1964
1976
  }
1965
1977
 
1978
+ /* File nodes — delimited-data tables and non-text placeholders */
1979
+ .file-table-shell {
1980
+ display: flex;
1981
+ flex-direction: column;
1982
+ height: 100%;
1983
+ min-height: 0;
1984
+ }
1985
+
1986
+ .file-node-notice {
1987
+ flex-shrink: 0;
1988
+ padding: 4px 10px;
1989
+ color: var(--c-muted);
1990
+ font-size: 10px;
1991
+ }
1992
+
1993
+ /* Own scroll port so a wide CSV scrolls inside the node instead of blowing out
1994
+ the layout — and so the sticky header row has something to stick to. */
1995
+ .file-table-wrap {
1996
+ flex: 1;
1997
+ min-height: 0;
1998
+ overflow: auto;
1999
+ }
2000
+
2001
+ /* The shell/wrap prefix is load-bearing: these rules must out-specify the markdown
2002
+ table styles (`.canvas-node .node-body table` / `th` / `td`), whose 0.5em
2003
+ margin and translucent header background break the sticky header. Borders are
2004
+ separate, not collapsed — collapsed borders belong to the table element, so
2005
+ the header leaks the rows scrolling beneath it. */
2006
+ .file-table-shell .file-table-wrap .file-table {
2007
+ margin: 0;
2008
+ border-collapse: separate;
2009
+ border-spacing: 0;
2010
+ width: max-content;
2011
+ min-width: 100%;
2012
+ font-family: var(--mono);
2013
+ color: var(--c-text);
2014
+ }
2015
+
2016
+ .file-table-shell .file-table-wrap .file-table th,
2017
+ .file-table-shell .file-table-wrap .file-table td {
2018
+ max-width: 320px;
2019
+ padding: 3px 8px;
2020
+ border: none;
2021
+ border-right: 1px solid var(--c-line);
2022
+ border-bottom: 1px solid var(--c-line);
2023
+ text-align: left;
2024
+ white-space: nowrap;
2025
+ overflow: hidden;
2026
+ text-overflow: ellipsis;
2027
+ }
2028
+
2029
+ .file-table-shell .file-table-wrap .file-table th {
2030
+ position: sticky;
2031
+ top: 0;
2032
+ z-index: 1;
2033
+ /* Opaque in every theme — rows must not bleed through while scrolling. */
2034
+ background: var(--c-panel);
2035
+ color: var(--c-text-soft);
2036
+ font-weight: 600;
2037
+ }
2038
+
2039
+ .file-table-shell .file-table-wrap .file-table tbody tr:nth-child(even) {
2040
+ background: var(--c-surface-subtle);
2041
+ }
2042
+
2043
+ .file-node-placeholder {
2044
+ display: flex;
2045
+ flex-direction: column;
2046
+ align-items: center;
2047
+ justify-content: center;
2048
+ gap: 4px;
2049
+ height: 100%;
2050
+ padding: 16px;
2051
+ color: var(--c-muted);
2052
+ text-align: center;
2053
+ }
2054
+
2055
+ .file-node-placeholder-name {
2056
+ color: var(--c-text-soft);
2057
+ word-break: break-all;
2058
+ }
2059
+
2060
+ .file-node-link {
2061
+ color: var(--c-accent);
2062
+ }
2063
+
1966
2064
  /* Docked node — compact toolbar-flanking panel */
1967
2065
  .docked-node {
1968
2066
  min-width: 200px;