pmx-canvas 0.10.0 → 0.11.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/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/.github/extensions/pmx-canvas/tools.json +246 -108
- package/CHANGELOG.md +32 -0
- package/dist/canvas/chunks/abnfDiagram-N423BO3Z-p8dtp6va.js +1 -0
- package/dist/canvas/chunks/architecture-TIHT7OUA-9tffvpsv.js +1 -0
- package/dist/canvas/chunks/architectureDiagram-T3A2C74G-5jhmxrzf.js +36 -0
- package/dist/canvas/chunks/blockDiagram-VBNYF7ZC-pw1fa9yb.js +132 -0
- package/dist/canvas/chunks/c4Diagram-5PPSVZJV-x07nq0tt.js +10 -0
- package/dist/canvas/chunks/classDiagram-JCYQIIEL-nc6g5x1r.js +1 -0
- package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-0qnbpwvh.js +1 -0
- package/dist/canvas/chunks/cose-bilkent-JH36ORCC-nm39npq0.js +1 -0
- package/dist/canvas/chunks/cynefin-VYW2F7L2-ys8p46ts.js +1 -0
- package/dist/canvas/chunks/cynefinDiagram-MW4NZA55-vdxrwahz.js +62 -0
- package/dist/canvas/chunks/dagre-VZM6K2ZE-bqdm7wpd.js +4 -0
- package/dist/canvas/chunks/diagram-7IWD3JNH-gs5gpr93.js +30 -0
- package/dist/canvas/chunks/diagram-B4RE2ZJO-cgwjg47s.js +3 -0
- package/dist/canvas/chunks/diagram-LBJQPF4R-w3vahpav.js +24 -0
- package/dist/canvas/chunks/diagram-Q27KOJAE-42837z6h.js +24 -0
- package/dist/canvas/chunks/diagram-UB23O5K3-4fz83q59.js +41 -0
- package/dist/canvas/chunks/ebnfDiagram-BXEA7PRR-zphtgegj.js +1 -0
- package/dist/canvas/chunks/erDiagram-JOGREHBK-zvbbdxp4.js +85 -0
- package/dist/canvas/chunks/eventmodeling-45OFAUF4-v9q6dn4c.js +1 -0
- package/dist/canvas/chunks/flowDiagram-UKHOOZJN-yrr5z8tc.js +1 -0
- package/dist/canvas/chunks/ganttDiagram-PKOTCBZU-282jzycm.js +292 -0
- package/dist/canvas/chunks/gitGraph-TEB2WS4Q-kdwb8m0a.js +1 -0
- package/dist/canvas/chunks/gitGraphDiagram-DS77QQ5N-vqqy6p1b.js +106 -0
- package/dist/canvas/chunks/index-0rk4gp1g.js +2 -0
- package/dist/canvas/chunks/index-194m3hc3.js +2 -0
- package/dist/canvas/chunks/index-1r72hdzb.js +301 -0
- package/dist/canvas/chunks/index-353m7b0p.js +2 -0
- package/dist/canvas/chunks/index-3cy1bzwh.js +2 -0
- package/dist/canvas/chunks/index-5tv8v1hk.js +1 -0
- package/dist/canvas/chunks/index-6ceya556.js +332 -0
- package/dist/canvas/chunks/index-6qmsnxwy.js +4 -0
- package/dist/canvas/chunks/index-d3b3gqe9.js +2 -0
- package/dist/canvas/chunks/index-fjtdggjn.js +2 -0
- package/dist/canvas/chunks/index-g3pwnqeb.js +2 -0
- package/dist/canvas/chunks/index-gf4e0et4.js +2 -0
- package/dist/canvas/chunks/index-hj99nz1f.js +1 -0
- package/dist/canvas/chunks/index-nprm96xp.js +16 -0
- package/dist/canvas/chunks/index-qj8fxwsf.js +2 -0
- package/dist/canvas/chunks/index-r2fyc9w1.js +89 -0
- package/dist/canvas/chunks/index-rft2hcz5.js +2 -0
- package/dist/canvas/chunks/index-rh7nv68n.js +2 -0
- package/dist/canvas/chunks/index-s4j678gj.js +2 -0
- package/dist/canvas/chunks/index-tb1wf525.js +179 -0
- package/dist/canvas/chunks/index-vv56h019.js +2 -0
- package/dist/canvas/chunks/index-w2mra17t.js +207 -0
- package/dist/canvas/chunks/index-wgv7en21.js +232 -0
- package/dist/canvas/chunks/index-yw8h9vha.js +157 -0
- package/dist/canvas/chunks/info-DKCQHKI2-wpzs6sz3.js +1 -0
- package/dist/canvas/chunks/infoDiagram-6WML65LV-v1yvggqj.js +2 -0
- package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-5hnq1a9p.js +70 -0
- package/dist/canvas/chunks/journeyDiagram-NVQOT4AX-zjcffveh.js +139 -0
- package/dist/canvas/chunks/kanban-definition-27J2QSJJ-dakqtfp1.js +89 -0
- package/dist/canvas/chunks/katex-dmg8k4zm.js +257 -0
- package/dist/canvas/chunks/mermaid.core-j2geswrd.js +12 -0
- package/dist/canvas/chunks/mindmap-definition-FAOFIHXS-142xhr3k.js +96 -0
- package/dist/canvas/chunks/packet-7NZHBO7P-xn439f0s.js +1 -0
- package/dist/canvas/chunks/pegDiagram-VL7TDLO6-5wvk7vsd.js +1 -0
- package/dist/canvas/chunks/pie-RZYD4A2V-0kf16rwh.js +1 -0
- package/dist/canvas/chunks/pieDiagram-7S7Q4E2Y-ztdhwycc.js +39 -0
- package/dist/canvas/chunks/quadrantDiagram-CIZ2JOQS-c5p9gmg6.js +7 -0
- package/dist/canvas/chunks/radar-I7S5WNFK-0g2pz36t.js +1 -0
- package/dist/canvas/chunks/railroad-3IZDKUUU-fcaran6j.js +1 -0
- package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-wgdya41q.js +1 -0
- package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-gy07a4ka.js +1 -0
- package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-ewk6wtkh.js +1 -0
- package/dist/canvas/chunks/railroadDiagram-AXF67PYL-xkmqkjwf.js +1 -0
- package/dist/canvas/chunks/requirementDiagram-LRYGKXZP-9wm2dyaw.js +84 -0
- package/dist/canvas/chunks/sankeyDiagram-W5VNT64P-2w19etgb.js +40 -0
- package/dist/canvas/chunks/sequenceDiagram-SI44F4Z6-0jn0vwmz.js +162 -0
- package/dist/canvas/chunks/sizeCapture-X5ZJPWSS-x75epwdq.js +1 -0
- package/dist/canvas/chunks/stateDiagram-OKZ733FA-schjpy6d.js +1 -0
- package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-k8sfp2e7.js +1 -0
- package/dist/canvas/chunks/swimlanes-SLNWSIFB-gkw4xce3.js +1 -0
- package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-nqysw67j.js +8 -0
- package/dist/canvas/chunks/timeline-definition-Z64GVDOM-x7nkcy01.js +120 -0
- package/dist/canvas/chunks/treeView-QDETBFTQ-cesw6gp0.js +1 -0
- package/dist/canvas/chunks/treemap-6X3UGDF4-zdv4knp9.js +1 -0
- package/dist/canvas/chunks/vennDiagram-T6HMQDX7-x8dqn9yf.js +34 -0
- package/dist/canvas/chunks/wardley-OPB4EBWU-44pdddez.js +1 -0
- package/dist/canvas/chunks/wardleyDiagram-T6FBY63Y-t1j52t9r.js +78 -0
- package/dist/canvas/chunks/xychartDiagram-ELKLHX3M-38t16pme.js +7 -0
- package/dist/canvas/global.css +389 -229
- package/dist/canvas/index.js +141 -211
- package/dist/canvas/mcp-app.html +4205 -554
- package/dist/canvas/mermaid-entry.js +338 -338
- package/dist/canvas/surface-theme.css +6 -6
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +49 -49
- package/dist/types/client/canvas/BarHint.d.ts +5 -3
- package/dist/types/client/canvas/CanvasNode.d.ts +0 -11
- package/dist/types/client/canvas/EdgeLayer.d.ts +1 -2
- package/dist/types/client/canvas/auto-fit.d.ts +4 -5
- package/dist/types/client/mermaid-entry.d.ts +1 -2
- package/dist/types/client/mermaid-render.d.ts +1 -0
- package/dist/types/client/nodes/BoardNode.d.ts +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +7 -90
- package/dist/types/client/nodes/McpAppNode.d.ts +1 -25
- package/dist/types/client/nodes/MermaidNode.d.ts +1 -8
- package/dist/types/client/nodes/ViewerFrame.d.ts +29 -0
- package/dist/types/client/nodes/ViewerHost.d.ts +6 -0
- package/dist/types/client/nodes/iframe-document-url.d.ts +3 -9
- package/dist/types/client/nodes/primitive-copy.d.ts +5 -0
- package/dist/types/client/nodes/use-iframe-content-height.d.ts +1 -1
- package/dist/types/client/nodes/use-surface-frame.d.ts +4 -6
- package/dist/types/client/nodes/viewer-runtime-state.d.ts +2 -1
- package/dist/types/client/state/canvas-store.d.ts +3 -1
- package/dist/types/client/state/intent-bridge.d.ts +1 -0
- package/dist/types/client/state/presentation.d.ts +17 -6
- package/dist/types/json-render/server.d.ts +2 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/index.d.ts +10 -5
- package/dist/types/server/operations/ops/boards.d.ts +0 -1
- package/dist/types/server/tour-control.d.ts +13 -3
- package/dist/types/shared/ax-surface-protocol.d.ts +0 -9
- package/dist/types/shared/tour.d.ts +8 -0
- package/docs/design/Chrome.dc.html +32 -6
- package/docs/design/Types1.dc.html +3 -2
- package/docs/design/Types2.dc.html +1 -1
- package/docs/design/Types3.dc.html +1 -1
- package/docs/design/Zoom.dc.html +37 -60
- package/docs/design/canvas.json +1 -1
- package/docs/design.md +169 -4
- package/docs/http-api.md +51 -1
- package/docs/mcp.md +8 -0
- package/docs/node-types.md +38 -3
- package/docs/plans/plan-009-tech-debt-backlog.md +2 -2
- package/docs/plans/plan-019-post-0.9-roadmap.md +283 -11
- package/docs/product-vision-2026-09.md +9 -0
- package/package.json +2 -2
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/plugin.json +1 -1
- package/plugins/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/tools.json +246 -108
- package/skills/pmx-canvas/SKILL.md +11 -18
- package/skills/pmx-canvas/references/full-reference.md +9 -4
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +3 -5
- package/src/client/canvas/BarHint.tsx +6 -3
- package/src/client/canvas/CanvasNode.tsx +34 -47
- package/src/client/canvas/CanvasViewport.tsx +6 -7
- package/src/client/canvas/ContextMenu.tsx +63 -3
- package/src/client/canvas/EdgeLayer.tsx +365 -169
- package/src/client/canvas/ExpandedNodeOverlay.tsx +1 -2
- package/src/client/canvas/Presentation.tsx +53 -3
- package/src/client/canvas/TopBar.tsx +24 -9
- package/src/client/canvas/auto-fit.ts +9 -11
- package/src/client/canvas/canvas-area.ts +1 -1
- package/src/client/canvas/use-focus-trap.ts +6 -1
- package/src/client/mermaid-entry.ts +6 -20
- package/src/client/mermaid-render.ts +39 -0
- package/src/client/nodes/AxStepControls.tsx +45 -69
- package/src/client/nodes/BoardNode.tsx +65 -9
- package/src/client/nodes/ContextNode.tsx +88 -396
- package/src/client/nodes/ExtAppFrame.tsx +283 -1462
- package/src/client/nodes/FileNode.tsx +92 -81
- package/src/client/nodes/HtmlNode.tsx +8 -36
- package/src/client/nodes/LedgerNode.tsx +31 -17
- package/src/client/nodes/MarkdownNode.tsx +8 -17
- package/src/client/nodes/McpAppNode.tsx +48 -154
- package/src/client/nodes/MermaidNode.tsx +80 -69
- package/src/client/nodes/PromptNode.tsx +12 -14
- package/src/client/nodes/ResponseNode.tsx +8 -6
- package/src/client/nodes/StatusNode.tsx +22 -7
- package/src/client/nodes/StatusSummary.tsx +5 -3
- package/src/client/nodes/TraceNode.tsx +8 -8
- package/src/client/nodes/ViewerFrame.tsx +262 -0
- package/src/client/nodes/ViewerHost.tsx +41 -0
- package/src/client/nodes/WebpageNode.tsx +22 -24
- package/src/client/nodes/iframe-document-url.ts +20 -27
- package/src/client/nodes/primitive-copy.ts +97 -0
- package/src/client/nodes/use-iframe-content-height.ts +34 -10
- package/src/client/nodes/use-surface-frame.ts +22 -11
- package/src/client/nodes/viewer-runtime-state.ts +41 -35
- package/src/client/state/canvas-store.ts +18 -12
- package/src/client/state/intent-bridge.ts +1 -0
- package/src/client/state/presentation.ts +135 -15
- package/src/client/state/sse-bridge.ts +18 -19
- package/src/client/theme/global.css +389 -229
- package/src/client/theme/surface-theme.css +6 -6
- package/src/json-render/charts/tufte-components.tsx +44 -2
- package/src/json-render/renderer/index.css +62 -2
- package/src/json-render/renderer/index.tsx +31 -7
- package/src/json-render/server.ts +3 -2
- package/src/server/board-export.ts +35 -24
- package/src/server/canvas-geometry.ts +1 -0
- package/src/server/canvas-state.ts +5 -0
- package/src/server/demo-state.json +106 -100
- package/src/server/html-primitives.ts +3 -9
- package/src/server/html-surface.ts +8 -0
- package/src/server/index.ts +19 -9
- package/src/server/operations/mcp-contracts.ts +17 -2
- package/src/server/operations/ops/boards.ts +1 -3
- package/src/server/operations/ops/viewport.ts +11 -9
- package/src/server/server.ts +7 -59
- package/src/server/tour-control.ts +66 -6
- package/src/shared/ax-surface-protocol.ts +0 -11
- package/src/shared/content-height-reporter.ts +4 -3
- package/src/shared/tour.ts +8 -0
|
@@ -4,6 +4,7 @@ import { canvasArea } from './canvas-area';
|
|
|
4
4
|
import { cancelViewportAnimation, hasInitialServerLayout, viewport } from '../state/canvas-store';
|
|
5
5
|
import {
|
|
6
6
|
endPresentation,
|
|
7
|
+
presentationCursor,
|
|
7
8
|
presentationError,
|
|
8
9
|
presenting,
|
|
9
10
|
recordingCamera,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
stepTour,
|
|
12
13
|
} from '../state/presentation';
|
|
13
14
|
import { usePanZoom } from './use-pan-zoom';
|
|
15
|
+
import { BarHint } from './BarHint';
|
|
14
16
|
|
|
15
17
|
declare global {
|
|
16
18
|
interface Window {
|
|
@@ -25,6 +27,9 @@ declare global {
|
|
|
25
27
|
|
|
26
28
|
export function Presentation() {
|
|
27
29
|
const active = presenting.value;
|
|
30
|
+
const cursor = presentationCursor.value;
|
|
31
|
+
const canBack = cursor !== null && cursor.index > 0;
|
|
32
|
+
const canNext = cursor !== null && cursor.index < cursor.total - 1;
|
|
28
33
|
const ready = hasInitialServerLayout.value;
|
|
29
34
|
const moveCamera = (camera: Camera) => {
|
|
30
35
|
cancelViewportAnimation();
|
|
@@ -89,9 +94,54 @@ export function Presentation() {
|
|
|
89
94
|
}}
|
|
90
95
|
onContextMenu={(event) => event.preventDefault()}
|
|
91
96
|
>
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
<div class="presentation-controls" role="group" aria-label="Presentation controls">
|
|
98
|
+
<BarHint
|
|
99
|
+
id="presentation-back-hint"
|
|
100
|
+
label="Previous stop"
|
|
101
|
+
shortcut="← / ↑"
|
|
102
|
+
fitWithin=".presentation-navigation"
|
|
103
|
+
>
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
class="presentation-step"
|
|
107
|
+
aria-describedby="presentation-back-hint"
|
|
108
|
+
aria-disabled={!canBack}
|
|
109
|
+
onClick={() => {
|
|
110
|
+
if (canBack) void stepTour({ step: 'previous' });
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
Back
|
|
114
|
+
</button>
|
|
115
|
+
</BarHint>
|
|
116
|
+
<BarHint
|
|
117
|
+
id="presentation-next-hint"
|
|
118
|
+
label="Next stop"
|
|
119
|
+
shortcut="→ / ↓ / Space"
|
|
120
|
+
fitWithin=".presentation-navigation"
|
|
121
|
+
>
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
class="presentation-step"
|
|
125
|
+
aria-describedby="presentation-next-hint"
|
|
126
|
+
aria-disabled={!canNext}
|
|
127
|
+
onClick={() => {
|
|
128
|
+
if (canNext) void stepTour({ step: 'next' });
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
Next
|
|
132
|
+
</button>
|
|
133
|
+
</BarHint>
|
|
134
|
+
<BarHint id="presentation-exit-hint" label="Exit presentation" shortcut="Esc" align="end">
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
class="presentation-exit"
|
|
138
|
+
aria-describedby="presentation-exit-hint"
|
|
139
|
+
onClick={endPresentation}
|
|
140
|
+
>
|
|
141
|
+
Exit presentation
|
|
142
|
+
</button>
|
|
143
|
+
</BarHint>
|
|
144
|
+
</div>
|
|
95
145
|
</div>
|
|
96
146
|
)}
|
|
97
147
|
{presentationError.value && (
|
|
@@ -39,6 +39,14 @@ import {
|
|
|
39
39
|
import { formatCountdown, gateRemainingMs } from '../../shared/approval-gates.js';
|
|
40
40
|
import { agentSessionsEnabled } from '../state/workspace-settings';
|
|
41
41
|
|
|
42
|
+
const PRESENT_DESCRIPTION = (
|
|
43
|
+
<span style={{ whiteSpace: 'pre-line' }}>
|
|
44
|
+
{
|
|
45
|
+
'A presentation follows either a saved tour or the board’s groups.\n\nSaved tour: ask your agent to define the stops and their order. Each stop can focus on a card, a group, a rectangular area, or a specific camera position.\n\nNo saved tour: every group becomes a stop, ordered top-to-bottom, then left-to-right. It doesn’t automatically visit every card.'
|
|
46
|
+
}
|
|
47
|
+
</span>
|
|
48
|
+
);
|
|
49
|
+
|
|
42
50
|
/**
|
|
43
51
|
* The attached session's chip (rail-chrome-v2 phase 3): phase-colored, with a
|
|
44
52
|
* dot that pulses while the agent thinks or runs a tool. Renders ONLY while a
|
|
@@ -481,12 +489,16 @@ export function TopBar() {
|
|
|
481
489
|
)}
|
|
482
490
|
{activeBoardId.value && (
|
|
483
491
|
<BarHint
|
|
492
|
+
id="present-hint"
|
|
484
493
|
label="Present board"
|
|
485
|
-
body=
|
|
494
|
+
body={PRESENT_DESCRIPTION}
|
|
495
|
+
align="end"
|
|
496
|
+
fitWithin="body"
|
|
486
497
|
>
|
|
487
498
|
<button
|
|
488
499
|
type="button"
|
|
489
500
|
class="present-button"
|
|
501
|
+
aria-describedby="present-hint"
|
|
490
502
|
onClick={() => {
|
|
491
503
|
void startPresentation();
|
|
492
504
|
}}
|
|
@@ -592,14 +604,17 @@ function TopBarMore({ boardOpen, onFit }: { boardOpen: boolean; onFit: () => voi
|
|
|
592
604
|
</button>
|
|
593
605
|
)}
|
|
594
606
|
{boardOpen && (
|
|
595
|
-
<
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
607
|
+
<BarHint id="present-menu-hint" label="Present board" body={PRESENT_DESCRIPTION}>
|
|
608
|
+
<button
|
|
609
|
+
type="button"
|
|
610
|
+
role="menuitem"
|
|
611
|
+
class="toolbar-menu-item"
|
|
612
|
+
aria-describedby="present-menu-hint"
|
|
613
|
+
onClick={run(() => void startPresentation())}
|
|
614
|
+
>
|
|
615
|
+
Present board
|
|
616
|
+
</button>
|
|
617
|
+
</BarHint>
|
|
603
618
|
)}
|
|
604
619
|
<button type="button" role="menuitem" class="toolbar-menu-item" onClick={run(() => zoomByFactor(1.25))}>
|
|
605
620
|
Zoom in
|
|
@@ -7,12 +7,6 @@ export const AUTO_FIT_MAX_HEIGHT = 600;
|
|
|
7
7
|
// Iframe surfaces (charts/dashboards/rich html) can legitimately need more room
|
|
8
8
|
// than a text node, so they grow to a higher ceiling before scrolling.
|
|
9
9
|
export const AUTO_FIT_MAX_HEIGHT_IFRAME = 1400;
|
|
10
|
-
// `.node-body` adds 12px padding top+bottom around an iframe surface (global.css).
|
|
11
|
-
// The bridge reports the iframe's OWN document scrollHeight, so the grow target
|
|
12
|
-
// must add titlebar + this body padding or the node settles ~24px short and the
|
|
13
|
-
// surface shows a residual inner scrollbar. (The DOM auto-fit above doesn't need
|
|
14
|
-
// this: body.scrollHeight already includes the body's padding.)
|
|
15
|
-
export const AUTO_FIT_BODY_PADDING = 24;
|
|
16
10
|
|
|
17
11
|
/** Node types the DOM auto-fit can't measure: iframe-backed surfaces (html/
|
|
18
12
|
* json-render/graph/mcp-app), where the body's scrollHeight equals the iframe
|
|
@@ -77,13 +71,17 @@ export function shouldContentFitIframeNode(node: CanvasNodeState): boolean {
|
|
|
77
71
|
/**
|
|
78
72
|
* Grow-only target height from a surface-reported content height. Returns null
|
|
79
73
|
* when the node is exempt, the report is non-positive, or the node already fits
|
|
80
|
-
* (so it never shrinks
|
|
81
|
-
*
|
|
82
|
-
*
|
|
74
|
+
* (so it never shrinks). Callers measure the actual chrome in world pixels,
|
|
75
|
+
* including zoom-compensated headers and any captions. A fixed padding allowance
|
|
76
|
+
* makes a full-height viewer grow again on every content-height report.
|
|
83
77
|
*/
|
|
84
|
-
export function computeContentGrowHeight(
|
|
78
|
+
export function computeContentGrowHeight(
|
|
79
|
+
node: CanvasNodeState,
|
|
80
|
+
contentHeight: number,
|
|
81
|
+
chromeHeight: number,
|
|
82
|
+
): number | null {
|
|
85
83
|
if (!shouldContentFitIframeNode(node) || !Number.isFinite(contentHeight) || contentHeight <= 0) return null;
|
|
86
|
-
const want = Math.min(contentHeight +
|
|
84
|
+
const want = Math.min(Math.ceil(contentHeight + chromeHeight), AUTO_FIT_MAX_HEIGHT_IFRAME);
|
|
87
85
|
return want > node.size.height + 8 ? want : null;
|
|
88
86
|
}
|
|
89
87
|
|
|
@@ -71,7 +71,12 @@ export function useFocusTrap(
|
|
|
71
71
|
// from (focus mode unmounts the node while expanded) names the element
|
|
72
72
|
// to return to, which may only exist after the next frame.
|
|
73
73
|
if (restoreTo) {
|
|
74
|
-
requestAnimationFrame(() =>
|
|
74
|
+
requestAnimationFrame(() => {
|
|
75
|
+
// A newly opened menu owns focus before this deferred return runs.
|
|
76
|
+
if (document.activeElement === document.body || root.contains(document.activeElement)) {
|
|
77
|
+
restoreTo()?.focus();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
75
80
|
} else if (previous && document.contains(previous) && previous !== document.body) {
|
|
76
81
|
previous.focus();
|
|
77
82
|
}
|
|
@@ -5,16 +5,10 @@
|
|
|
5
5
|
* HTML-escaped diagram source in `<pre class="mermaid-source">`; this script
|
|
6
6
|
* reads it back via textContent, renders it to SVG, and re-renders when the
|
|
7
7
|
* parent canvas live-switches the theme (the theme bridge toggles the document's
|
|
8
|
-
* `data-theme` attribute).
|
|
9
|
-
* canvas boot lean — only mermaid-node surfaces pay for the library.
|
|
8
|
+
* `data-theme` attribute). Standalone views and exports share the canvas renderer.
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
|
-
import
|
|
13
|
-
import { canvasThemeScheme } from '../shared/themes.js';
|
|
14
|
-
|
|
15
|
-
function documentScheme(): 'dark' | 'light' {
|
|
16
|
-
return canvasThemeScheme(document.documentElement.getAttribute('data-theme'));
|
|
17
|
-
}
|
|
11
|
+
import { renderMermaid } from './mermaid-render.js';
|
|
18
12
|
|
|
19
13
|
function showError(container: HTMLElement, message: string): void {
|
|
20
14
|
container.innerHTML = '';
|
|
@@ -65,18 +59,14 @@ function fitDiagram(container: HTMLElement): void {
|
|
|
65
59
|
}
|
|
66
60
|
|
|
67
61
|
async function renderDiagram(source: string, container: HTMLElement): Promise<void> {
|
|
68
|
-
const
|
|
69
|
-
mermaid.initialize({
|
|
70
|
-
startOnLoad: false,
|
|
71
|
-
theme: scheme === 'dark' ? 'dark' : 'default',
|
|
72
|
-
securityLevel: 'strict',
|
|
73
|
-
});
|
|
74
|
-
renderSeq += 1;
|
|
62
|
+
const sequence = ++renderSeq;
|
|
75
63
|
try {
|
|
76
|
-
const
|
|
64
|
+
const svg = await renderMermaid(source);
|
|
65
|
+
if (sequence !== renderSeq) return;
|
|
77
66
|
container.innerHTML = svg;
|
|
78
67
|
fitDiagram(container);
|
|
79
68
|
} catch (error) {
|
|
69
|
+
if (sequence !== renderSeq) return;
|
|
80
70
|
// Never leave a blank/black frame: show the parse/render error instead.
|
|
81
71
|
showError(container, error instanceof Error ? error.message : String(error));
|
|
82
72
|
}
|
|
@@ -98,11 +88,7 @@ function boot(): void {
|
|
|
98
88
|
void renderDiagram(source, container);
|
|
99
89
|
// Live theme switching: the parent's theme bridge toggles data-theme on the
|
|
100
90
|
// root element; re-render so the diagram palette follows the canvas theme.
|
|
101
|
-
let lastScheme = documentScheme();
|
|
102
91
|
const observer = new MutationObserver(() => {
|
|
103
|
-
const scheme = documentScheme();
|
|
104
|
-
if (scheme === lastScheme) return;
|
|
105
|
-
lastScheme = scheme;
|
|
106
92
|
void renderDiagram(source, container);
|
|
107
93
|
});
|
|
108
94
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import DOMPurify from 'dompurify';
|
|
2
|
+
|
|
3
|
+
// Mermaid configuration is global: serialize configuration with each render.
|
|
4
|
+
let rendering: Promise<unknown> = Promise.resolve();
|
|
5
|
+
|
|
6
|
+
export function renderMermaid(source: string): Promise<string> {
|
|
7
|
+
const result = rendering.then(async () => {
|
|
8
|
+
// Diagram-only dependency: the SPA splits this module; self-contained MCP
|
|
9
|
+
// documents defer its initialization without requiring external assets.
|
|
10
|
+
const { default: mermaid } = await import('mermaid');
|
|
11
|
+
const style = getComputedStyle(document.documentElement);
|
|
12
|
+
const token = (name: string) => style.getPropertyValue(name).trim();
|
|
13
|
+
mermaid.initialize({
|
|
14
|
+
startOnLoad: false,
|
|
15
|
+
securityLevel: 'strict',
|
|
16
|
+
theme: 'base',
|
|
17
|
+
fontFamily: token('--font-ui'),
|
|
18
|
+
themeVariables: {
|
|
19
|
+
primaryColor: token('--c-panel-soft'),
|
|
20
|
+
primaryTextColor: token('--c-text'),
|
|
21
|
+
primaryBorderColor: token('--c-line'),
|
|
22
|
+
lineColor: token('--c-muted'),
|
|
23
|
+
secondaryColor: token('--c-panel'),
|
|
24
|
+
tertiaryColor: token('--c-panel'),
|
|
25
|
+
edgeLabelBackground: token('--c-panel'),
|
|
26
|
+
fontSize: '12px',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const { svg } = await mermaid.render(`mermaid-${crypto.randomUUID()}`, source);
|
|
30
|
+
return DOMPurify.sanitize(svg, {
|
|
31
|
+
ADD_TAGS: ['foreignObject'],
|
|
32
|
+
ADD_ATTR: ['dominant-baseline'],
|
|
33
|
+
HTML_INTEGRATION_POINTS: { foreignobject: true },
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
// A malformed diagram must not prevent the next diagram from rendering.
|
|
37
|
+
rendering = result.catch(() => {});
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
@@ -18,37 +18,10 @@ import { axNodeActionButtonStyle, runNodeAxInteraction } from './ax-node-actions
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* The rows sit in a
|
|
22
|
-
* node
|
|
23
|
-
*
|
|
24
|
-
* while still being INSIDE the body, which is what auto-fit measures.
|
|
21
|
+
* The rows sit in `.ax-step-dock`, a footer that stays reachable at the bottom of
|
|
22
|
+
* the node body (global.css). On a card it shares one footer row with the card's
|
|
23
|
+
* own actions; in the expanded overlay it is a sticky bar under the document.
|
|
25
24
|
*/
|
|
26
|
-
/**
|
|
27
|
-
* The dock reads as a real footer toolbar rather than stray text at the bottom
|
|
28
|
-
* edge. It bleeds to the container's edges via negative margins (`.node-body`
|
|
29
|
-
* has 12px padding; the expanded overlay's content area has 16px, and the dock
|
|
30
|
-
* is a sibling of that area, so it starts flush) and then re-applies its own
|
|
31
|
-
* padding — otherwise the first label sits hard against the left edge and looks
|
|
32
|
-
* clipped. `--c-panel-overlay` separates it from the content above it.
|
|
33
|
-
*/
|
|
34
|
-
const dockStyle = {
|
|
35
|
-
position: 'sticky',
|
|
36
|
-
// Bleed OUT of the container's padding so the bar spans edge to edge, then
|
|
37
|
-
// re-apply padding inside it. These are two different numbers: in a node body
|
|
38
|
-
// the dock sits INSIDE 12px of padding and must pull back out of it, while in
|
|
39
|
-
// the expanded overlay it is a sibling of the padded content area and is
|
|
40
|
-
// already flush — bleeding there would push it off the panel.
|
|
41
|
-
bottom: 'calc(-1 * var(--ax-dock-bleed, 12px))',
|
|
42
|
-
marginTop: '12px',
|
|
43
|
-
marginLeft: 'calc(-1 * var(--ax-dock-bleed, 12px))',
|
|
44
|
-
marginRight: 'calc(-1 * var(--ax-dock-bleed, 12px))',
|
|
45
|
-
marginBottom: 'calc(-1 * var(--ax-dock-bleed, 12px))',
|
|
46
|
-
padding: '9px var(--ax-dock-pad, 12px) 10px',
|
|
47
|
-
background: 'var(--c-panel-overlay)',
|
|
48
|
-
borderTop: '1px solid var(--c-line)',
|
|
49
|
-
backdropFilter: 'blur(6px)',
|
|
50
|
-
} as const;
|
|
51
|
-
|
|
52
25
|
const rowStyle = {
|
|
53
26
|
display: 'flex',
|
|
54
27
|
alignItems: 'center',
|
|
@@ -57,7 +30,7 @@ const rowStyle = {
|
|
|
57
30
|
} as const;
|
|
58
31
|
|
|
59
32
|
const labelStyle = {
|
|
60
|
-
fontSize: '
|
|
33
|
+
fontSize: 'var(--fs-meta)',
|
|
61
34
|
color: 'var(--c-muted)',
|
|
62
35
|
textTransform: 'uppercase',
|
|
63
36
|
letterSpacing: '0.04em',
|
|
@@ -160,47 +133,50 @@ function FlowLoopControls({ node, flow }: { node: CanvasNodeState; flow: AxFlowS
|
|
|
160
133
|
</button>
|
|
161
134
|
</BarHint>
|
|
162
135
|
)}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
e
|
|
173
|
-
|
|
174
|
-
}}
|
|
175
|
-
style={{
|
|
176
|
-
flex: '1 1 120px',
|
|
177
|
-
minWidth: '90px',
|
|
178
|
-
fontSize: '10px',
|
|
179
|
-
padding: '3px 6px',
|
|
180
|
-
borderRadius: '4px',
|
|
181
|
-
border: '1px solid var(--c-line)',
|
|
182
|
-
background: 'var(--c-panel-overlay)',
|
|
183
|
-
color: 'var(--c-text)',
|
|
184
|
-
}}
|
|
185
|
-
/>
|
|
186
|
-
<BarHint
|
|
187
|
-
label="Send a steering message to the agent working this flow"
|
|
188
|
-
side="up"
|
|
189
|
-
align="end"
|
|
190
|
-
fitWithin=".node-content, .expanded-overlay-panel"
|
|
191
|
-
>
|
|
192
|
-
<button
|
|
193
|
-
type="button"
|
|
194
|
-
class="ax-node-action"
|
|
195
|
-
style={axNodeActionButtonStyle}
|
|
196
|
-
onClick={(e) => {
|
|
136
|
+
{/* The draft and its send button wrap as one unit, never a lone Steer. */}
|
|
137
|
+
<span style={{ display: 'flex', gap: '6px', flex: '1 1 140px', minWidth: 0 }}>
|
|
138
|
+
<input
|
|
139
|
+
class="ax-flow-steer-input"
|
|
140
|
+
value={message}
|
|
141
|
+
placeholder="Steer the agent…"
|
|
142
|
+
aria-label="Steer the agent"
|
|
143
|
+
onInput={(e) => setMessage((e.target as HTMLInputElement).value)}
|
|
144
|
+
onClick={(e) => e.stopPropagation()}
|
|
145
|
+
onKeyDown={(e) => {
|
|
146
|
+
if (e.key !== 'Enter') return;
|
|
197
147
|
e.stopPropagation();
|
|
198
148
|
void handleSteer();
|
|
199
149
|
}}
|
|
150
|
+
style={{
|
|
151
|
+
flex: '1 1 0',
|
|
152
|
+
minWidth: 0,
|
|
153
|
+
fontSize: 'var(--fs-meta)',
|
|
154
|
+
padding: '3px 6px',
|
|
155
|
+
borderRadius: '4px',
|
|
156
|
+
border: '1px solid var(--c-line)',
|
|
157
|
+
background: 'var(--c-panel-overlay)',
|
|
158
|
+
color: 'var(--c-text)',
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
<BarHint
|
|
162
|
+
label="Send a steering message to the agent working this flow"
|
|
163
|
+
side="up"
|
|
164
|
+
align="end"
|
|
165
|
+
fitWithin=".node-content, .expanded-overlay-panel"
|
|
200
166
|
>
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
class="ax-node-action"
|
|
170
|
+
style={axNodeActionButtonStyle}
|
|
171
|
+
onClick={(e) => {
|
|
172
|
+
e.stopPropagation();
|
|
173
|
+
void handleSteer();
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
Steer
|
|
177
|
+
</button>
|
|
178
|
+
</BarHint>
|
|
179
|
+
</span>
|
|
204
180
|
</div>
|
|
205
181
|
);
|
|
206
182
|
}
|
|
@@ -212,7 +188,7 @@ export function AxStepControls({ node }: { node: CanvasNodeState }) {
|
|
|
212
188
|
const status = mirroredStatus(node.data);
|
|
213
189
|
|
|
214
190
|
return (
|
|
215
|
-
<div class="ax-step-dock"
|
|
191
|
+
<div class="ax-step-dock">
|
|
216
192
|
<div class="ax-step-controls" style={rowStyle}>
|
|
217
193
|
<span class="ax-step-position" style={labelStyle}>
|
|
218
194
|
Step {step.index}/{step.total}
|
|
@@ -1,20 +1,43 @@
|
|
|
1
1
|
import type { CanvasNodeState } from '../types';
|
|
2
|
-
import {
|
|
2
|
+
import { useState } from 'preact/hooks';
|
|
3
|
+
import { activeBoardId, boardList, openBoard } from '../state/boards-store';
|
|
4
|
+
import { updateNodeFromClient } from '../state/intent-bridge';
|
|
3
5
|
import { IconExternalLink } from '../icons';
|
|
4
6
|
|
|
5
|
-
/**
|
|
7
|
+
/** Target identity is the durable board id; a missing target can be relinked. */
|
|
6
8
|
export function BoardNode({ node }: { node: CanvasNodeState }) {
|
|
7
9
|
const boardId = typeof node.data.boardId === 'string' ? node.data.boardId : '';
|
|
8
10
|
const target = boardList.value.find((board) => board.id === boardId);
|
|
11
|
+
const [relinking, setRelinking] = useState(false);
|
|
12
|
+
const [error, setError] = useState('');
|
|
9
13
|
return (
|
|
10
14
|
<div class="board-node">
|
|
15
|
+
{target ? (
|
|
16
|
+
<div class="home-thumb" aria-hidden="true">
|
|
17
|
+
<div class="home-thumb-inner">
|
|
18
|
+
{(target.preview ?? []).map((rect, index) => (
|
|
19
|
+
<span
|
|
20
|
+
key={index}
|
|
21
|
+
class={`home-thumb-card${rect.pinned ? ' is-pinned' : ''}`}
|
|
22
|
+
style={{
|
|
23
|
+
left: `${rect.x * 100}%`,
|
|
24
|
+
top: `${rect.y * 100}%`,
|
|
25
|
+
width: `${rect.w * 100}%`,
|
|
26
|
+
height: `${rect.h * 100}%`,
|
|
27
|
+
}}
|
|
28
|
+
/>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
) : (
|
|
33
|
+
<div class="board-node-missing">Board not found</div>
|
|
34
|
+
)}
|
|
11
35
|
<div class="board-node-content">
|
|
12
|
-
<
|
|
13
|
-
<strong class="board-node-title">{target?.name ?? 'Missing board'}</strong>
|
|
36
|
+
<strong class="board-node-title">{target?.name ?? String(node.data.title || 'Linked board')}</strong>
|
|
14
37
|
<div class="board-node-meta">
|
|
15
38
|
{target
|
|
16
|
-
?
|
|
17
|
-
:
|
|
39
|
+
? `Board · ${target.nodeCount} nodes${target.pin ? ' · pinned, so it travels with you' : ''}`
|
|
40
|
+
: 'The linked board is no longer available.'}
|
|
18
41
|
</div>
|
|
19
42
|
{target?.summary && <p class="board-node-summary">{target.summary}</p>}
|
|
20
43
|
{!!target?.pinnedTitles?.length && (
|
|
@@ -25,14 +48,47 @@ export function BoardNode({ node }: { node: CanvasNodeState }) {
|
|
|
25
48
|
Linked from: {[...new Set(target.backlinks.map((link) => link.title))].join(', ')}
|
|
26
49
|
</p>
|
|
27
50
|
)}
|
|
51
|
+
{relinking && (
|
|
52
|
+
<select
|
|
53
|
+
aria-label="Link to board"
|
|
54
|
+
class="board-node-relink"
|
|
55
|
+
value=""
|
|
56
|
+
onChange={async (event) => {
|
|
57
|
+
const id = event.currentTarget.value;
|
|
58
|
+
if (!id) return;
|
|
59
|
+
setError('');
|
|
60
|
+
const result = await updateNodeFromClient(node.id, {
|
|
61
|
+
data: { boardId: id },
|
|
62
|
+
expectedBoardId: activeBoardId.value,
|
|
63
|
+
});
|
|
64
|
+
if (result.ok) setRelinking(false);
|
|
65
|
+
else setError('Could not relink this card. Try again.');
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<option value="" disabled>
|
|
69
|
+
Choose board…
|
|
70
|
+
</option>
|
|
71
|
+
{boardList.value
|
|
72
|
+
.filter((board) => board.id !== activeBoardId.value)
|
|
73
|
+
.map((board) => (
|
|
74
|
+
<option key={board.id} value={board.id}>
|
|
75
|
+
{board.name}
|
|
76
|
+
</option>
|
|
77
|
+
))}
|
|
78
|
+
</select>
|
|
79
|
+
)}
|
|
80
|
+
{error && (
|
|
81
|
+
<div role="status" class="board-node-meta">
|
|
82
|
+
{error}
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
28
85
|
</div>
|
|
29
86
|
<button
|
|
30
87
|
class="board-node-open"
|
|
31
88
|
type="button"
|
|
32
|
-
|
|
33
|
-
onClick={() => target && void openBoard(target.id)}
|
|
89
|
+
onClick={() => (target ? void openBoard(target.id) : setRelinking(!relinking))}
|
|
34
90
|
>
|
|
35
|
-
{target ? 'Open board' : '
|
|
91
|
+
{target ? 'Open board' : 'Link to board…'}
|
|
36
92
|
{target && <IconExternalLink size={14} />}
|
|
37
93
|
</button>
|
|
38
94
|
</div>
|