pmx-canvas 0.9.3 → 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 +9418 -2016
- package/CHANGELOG.md +54 -1
- 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/mcp/canvas-access.d.ts +1 -12
- package/dist/types/mcp/resource-subscriptions.d.ts +7 -0
- package/dist/types/mcp/server.d.ts +2 -0
- package/dist/types/server/ax-wait.d.ts +1 -0
- package/dist/types/server/canvas-operations.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/index.d.ts +11 -6
- package/dist/types/server/operations/http.d.ts +10 -0
- package/dist/types/server/operations/index.d.ts +2 -2
- package/dist/types/server/operations/invoker.d.ts +4 -3
- package/dist/types/server/operations/mcp-contracts.d.ts +20 -0
- package/dist/types/server/operations/mcp.d.ts +2 -0
- package/dist/types/server/operations/ops/batch.d.ts +1 -0
- package/dist/types/server/operations/ops/boards.d.ts +0 -1
- package/dist/types/server/operations/registry.d.ts +2 -2
- package/dist/types/server/operations/types.d.ts +12 -2
- package/dist/types/server/server.d.ts +2 -0
- package/dist/types/server/tour-control.d.ts +13 -3
- package/dist/types/server/webpage-node.d.ts +1 -0
- package/dist/types/shared/ax-surface-protocol.d.ts +0 -9
- package/dist/types/shared/tour.d.ts +8 -0
- package/docs/api-reference.md +2 -2
- package/docs/cli.md +1 -1
- 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 +82 -1
- package/docs/mcp.md +101 -21
- 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 +402 -13
- package/docs/product-vision-2026-09.md +9 -0
- package/docs/sdk.md +5 -2
- 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 +9418 -2016
- package/skills/pmx-canvas/SKILL.md +19 -21
- package/skills/pmx-canvas/references/full-reference.md +44 -6
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +7 -7
- 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/ToolRail.tsx +2 -0
- 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/mcp/canvas-access.ts +6 -47
- package/src/mcp/resource-subscriptions.ts +181 -0
- package/src/mcp/server.ts +185 -219
- package/src/server/ax-wait.ts +21 -6
- package/src/server/board-export.ts +35 -24
- package/src/server/canvas-geometry.ts +1 -0
- package/src/server/canvas-operations.ts +6 -2
- 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 +30 -12
- package/src/server/operations/http.ts +69 -21
- package/src/server/operations/index.ts +8 -1
- package/src/server/operations/invoker.ts +53 -5
- package/src/server/operations/mcp-contracts.ts +354 -0
- package/src/server/operations/mcp.ts +153 -18
- package/src/server/operations/ops/ax-await.ts +18 -10
- package/src/server/operations/ops/ax-read.ts +2 -0
- package/src/server/operations/ops/ax-timeline.ts +2 -2
- package/src/server/operations/ops/batch.ts +27 -8
- package/src/server/operations/ops/boards.ts +1 -3
- package/src/server/operations/ops/nodes.ts +18 -3
- package/src/server/operations/ops/viewport.ts +11 -9
- package/src/server/operations/registry.ts +24 -4
- package/src/server/operations/types.ts +16 -2
- package/src/server/server.ts +17 -59
- package/src/server/tour-control.ts +66 -6
- package/src/server/webpage-node.ts +9 -4
- 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
|
@@ -22,21 +22,35 @@ export function useIframeContentHeight(
|
|
|
22
22
|
node: CanvasNodeState,
|
|
23
23
|
iframeRef: { current: HTMLIFrameElement | null },
|
|
24
24
|
frameToken: string,
|
|
25
|
-
): void {
|
|
25
|
+
): () => void {
|
|
26
26
|
useEffect(() => {
|
|
27
27
|
if (!frameToken) return undefined;
|
|
28
28
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
29
|
-
let
|
|
29
|
+
let pending: { frame: HTMLIFrameElement; height: number } | null = null;
|
|
30
|
+
let lastMeasurement: { frame: HTMLIFrameElement; height: number; width: number; chrome: number } | null = null;
|
|
30
31
|
const held = () =>
|
|
31
32
|
grabbingNodeId.value === node.id || otherHumans.value.some((human) => human.grabbingNodeId === node.id);
|
|
32
33
|
function apply() {
|
|
33
34
|
timer = null;
|
|
34
|
-
if (held() ||
|
|
35
|
-
const reported =
|
|
36
|
-
|
|
35
|
+
if (held() || pending === null) return;
|
|
36
|
+
const { frame, height: reported } = pending;
|
|
37
|
+
pending = null;
|
|
38
|
+
if (frame !== iframeRef.current) return;
|
|
37
39
|
const current = nodes.value.get(node.id);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
const card = frame.closest<HTMLElement>('.canvas-node');
|
|
41
|
+
if (!current || !card) return;
|
|
42
|
+
const chrome = card.offsetHeight - frame.clientHeight;
|
|
43
|
+
// Undo changes the viewport, which makes the reporter resend unchanged
|
|
44
|
+
// content. A replacement document owns a fresh fit even at the same size.
|
|
45
|
+
if (
|
|
46
|
+
lastMeasurement?.frame === frame &&
|
|
47
|
+
lastMeasurement.height === reported &&
|
|
48
|
+
lastMeasurement.width === current.size.width &&
|
|
49
|
+
lastMeasurement.chrome === chrome
|
|
50
|
+
)
|
|
51
|
+
return;
|
|
52
|
+
lastMeasurement = { frame, height: reported, width: current.size.width, chrome };
|
|
53
|
+
const target = computeContentGrowHeight(current, reported, chrome);
|
|
40
54
|
if (target === null) return;
|
|
41
55
|
const position = contentFitPosition(current, target, Array.from(nodes.value.values()));
|
|
42
56
|
const moved = position.x !== current.position.x || position.y !== current.position.y;
|
|
@@ -49,18 +63,19 @@ export function useIframeContentHeight(
|
|
|
49
63
|
}
|
|
50
64
|
function schedule() {
|
|
51
65
|
if (timer !== null) clearTimeout(timer);
|
|
52
|
-
timer =
|
|
66
|
+
timer = pending !== null && !held() ? setTimeout(apply, 300) : null;
|
|
53
67
|
}
|
|
54
68
|
const dispose = effect(() => {
|
|
55
69
|
held();
|
|
56
70
|
schedule();
|
|
57
71
|
});
|
|
58
72
|
function onMessage(event: MessageEvent) {
|
|
59
|
-
|
|
73
|
+
const frame = iframeRef.current;
|
|
74
|
+
if (!frame || event.source !== frame.contentWindow) return;
|
|
60
75
|
const d = event.data as { source?: string; type?: string; token?: string; height?: unknown } | null;
|
|
61
76
|
if (!d || d.source !== 'pmx-canvas-frame' || d.type !== 'content-height' || d.token !== frameToken) return;
|
|
62
77
|
if (typeof d.height !== 'number' || !Number.isFinite(d.height) || d.height <= 0) return;
|
|
63
|
-
|
|
78
|
+
pending = { frame, height: d.height };
|
|
64
79
|
schedule();
|
|
65
80
|
}
|
|
66
81
|
window.addEventListener('message', onMessage);
|
|
@@ -70,4 +85,13 @@ export function useIframeContentHeight(
|
|
|
70
85
|
if (timer !== null) clearTimeout(timer);
|
|
71
86
|
};
|
|
72
87
|
}, [node.id, frameToken]);
|
|
88
|
+
// Pending documents can report before promotion, when they are deliberately
|
|
89
|
+
// not the active ref. Ask the promoted document for a fresh measurement.
|
|
90
|
+
return () => {
|
|
91
|
+
if (!frameToken) return;
|
|
92
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
93
|
+
{ source: 'pmx-canvas-frame', type: 'measure-content', token: frameToken },
|
|
94
|
+
'*',
|
|
95
|
+
);
|
|
96
|
+
};
|
|
73
97
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'preact/hooks';
|
|
2
2
|
import { iframeMode } from '../state/iframe-mode';
|
|
3
3
|
import { isHostedWorkbench, workbenchFetch } from '../state/workbench-transport';
|
|
4
|
+
import { VIEWER_LOAD_DEADLINE_MS, type ViewerSource } from './ViewerFrame';
|
|
4
5
|
|
|
5
6
|
function isSameOriginUrl(url: string): boolean {
|
|
6
7
|
if (isHostedWorkbench()) return url.startsWith('/') && !url.startsWith('//');
|
|
@@ -17,34 +18,45 @@ function isSameOriginUrl(url: string): boolean {
|
|
|
17
18
|
* iframes are blocked by the embedding context (nested-iframe hosts like Amp
|
|
18
19
|
* orb portals). Cross-origin URLs always stay `src` — they cannot be fetched
|
|
19
20
|
* from here. While the probe is pending, same-origin frames stay empty rather
|
|
20
|
-
* than loading a src that may show the broken placeholder
|
|
21
|
-
*
|
|
21
|
+
* than loading a src that may show the broken placeholder. Preparation failures
|
|
22
|
+
* use the shared host's Retry, never a second load path the embed probe rejected.
|
|
22
23
|
*/
|
|
23
|
-
export function useSurfaceFrame(url: string):
|
|
24
|
+
export function useSurfaceFrame(url: string): ViewerSource {
|
|
24
25
|
const mode = iframeMode.value;
|
|
25
26
|
const [doc, setDoc] = useState<{ url: string; html: string } | null>(null);
|
|
27
|
+
const [error, setError] = useState(false);
|
|
28
|
+
const [attempt, setAttempt] = useState(0);
|
|
26
29
|
const sameOrigin = Boolean(url) && isSameOriginUrl(url);
|
|
27
30
|
const wantsSrcdoc = mode === 'srcdoc' && sameOrigin;
|
|
28
31
|
|
|
29
32
|
useEffect(() => {
|
|
33
|
+
setError(false);
|
|
30
34
|
if (!wantsSrcdoc) return;
|
|
31
35
|
let cancelled = false;
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
const deadline = setTimeout(() => controller.abort(), VIEWER_LOAD_DEADLINE_MS);
|
|
32
38
|
// Ask the server to inline bundle assets (the mermaid renderer): hosts
|
|
33
39
|
// that force srcdoc mode block sub-frame subresource requests too.
|
|
34
|
-
void workbenchFetch(`${url}${url.includes('?') ? '&' : '?'}inline-assets=1`, {
|
|
40
|
+
void workbenchFetch(`${url}${url.includes('?') ? '&' : '?'}inline-assets=1`, {
|
|
41
|
+
credentials: 'same-origin',
|
|
42
|
+
signal: controller.signal,
|
|
43
|
+
})
|
|
35
44
|
.then((res) => (res.ok ? res.text() : Promise.reject(new Error(`HTTP ${res.status}`))))
|
|
36
45
|
.then((html) => {
|
|
37
46
|
if (!cancelled) setDoc({ url, html });
|
|
38
47
|
})
|
|
39
48
|
.catch((error) => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
})
|
|
49
|
+
if (cancelled) return;
|
|
50
|
+
console.error('[surface-frame] document fetch failed:', url, error);
|
|
51
|
+
setError(true);
|
|
52
|
+
})
|
|
53
|
+
.finally(() => clearTimeout(deadline));
|
|
44
54
|
return () => {
|
|
45
55
|
cancelled = true;
|
|
56
|
+
clearTimeout(deadline);
|
|
57
|
+
controller.abort();
|
|
46
58
|
};
|
|
47
|
-
}, [wantsSrcdoc, url]);
|
|
59
|
+
}, [wantsSrcdoc, url, attempt]);
|
|
48
60
|
|
|
49
61
|
if (!url) return {};
|
|
50
62
|
if (!wantsSrcdoc) {
|
|
@@ -52,6 +64,5 @@ export function useSurfaceFrame(url: string): { src?: string; srcdoc?: string }
|
|
|
52
64
|
}
|
|
53
65
|
// Keep the previous document visible while a newer URL is being fetched —
|
|
54
66
|
// same UX as src mode, where the old document stays until the new one loads.
|
|
55
|
-
|
|
56
|
-
return {};
|
|
67
|
+
return { srcdoc: doc?.html, error, retry: () => setAttempt((value) => value + 1) };
|
|
57
68
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { effect } from '@preact/signals';
|
|
2
2
|
import { JSON_VIEWER_STATE_SOURCE } from '../../shared/ax-surface-protocol.js';
|
|
3
3
|
import { canvasInteractionEpoch, nodes } from '../state/canvas-store';
|
|
4
|
+
import type { CanvasNodeState } from '../types';
|
|
5
|
+
import { VIEWER_LOAD_DEADLINE_MS } from './ViewerFrame';
|
|
4
6
|
|
|
5
7
|
type Snapshot = {
|
|
6
8
|
epoch: number;
|
|
7
9
|
version: unknown;
|
|
8
10
|
documentUrl: unknown;
|
|
9
11
|
state: Record<string, unknown> | null;
|
|
10
|
-
owners: Array<{ token: string; source: Window
|
|
12
|
+
owners: Array<{ token: string; source: Window }>;
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
// Tab-local UI state only: never persist form drafts in canvas data or AX. Keep
|
|
@@ -18,11 +20,12 @@ const snapshots = new Map<string, Snapshot>();
|
|
|
18
20
|
let listening = false;
|
|
19
21
|
|
|
20
22
|
export function restoreViewerRuntimeState(
|
|
21
|
-
|
|
23
|
+
authored: CanvasNodeState,
|
|
22
24
|
token: string,
|
|
23
25
|
source: Window,
|
|
24
26
|
signal?: AbortSignal,
|
|
25
27
|
): Promise<boolean | 'cancelled'> {
|
|
28
|
+
const nodeId = authored.id;
|
|
26
29
|
if (!listening) {
|
|
27
30
|
listening = true;
|
|
28
31
|
effect(() => {
|
|
@@ -36,7 +39,6 @@ export function restoreViewerRuntimeState(
|
|
|
36
39
|
snapshot.version !== node.data.specVersion ||
|
|
37
40
|
snapshot.documentUrl !== node.data.url
|
|
38
41
|
) {
|
|
39
|
-
for (const owner of snapshot.owners) owner.cancel();
|
|
40
42
|
snapshots.delete(id);
|
|
41
43
|
}
|
|
42
44
|
}
|
|
@@ -54,57 +56,61 @@ export function restoreViewerRuntimeState(
|
|
|
54
56
|
const ownerIndex =
|
|
55
57
|
snapshot?.owners.findIndex((owner) => owner.token === message.token && owner.source === event.source) ?? -1;
|
|
56
58
|
if (!snapshot || ownerIndex < 0) return;
|
|
57
|
-
if (message.type === 'mounted') {
|
|
58
|
-
snapshot.owners[ownerIndex]!.mounted();
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (message.type === 'ready') {
|
|
62
|
-
snapshot.owners[ownerIndex]!.ready();
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
59
|
if (message.type !== 'snapshot') return;
|
|
66
60
|
if (!message.state || typeof message.state !== 'object' || Array.isArray(message.state)) return;
|
|
67
61
|
const { ax: _ax, ...state } = message.state as Record<string, unknown>;
|
|
68
62
|
snapshot.state = state;
|
|
69
63
|
// Once the replacement emits, the retired document cannot overwrite it.
|
|
70
|
-
const retired = snapshot.owners.slice(0, ownerIndex);
|
|
71
64
|
snapshot.owners = snapshot.owners.slice(ownerIndex);
|
|
72
|
-
for (const owner of retired) owner.cancel();
|
|
73
65
|
});
|
|
74
66
|
}
|
|
75
67
|
const node = nodes.value.get(nodeId);
|
|
76
68
|
if (!node || signal?.aborted) return Promise.resolve('cancelled');
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
69
|
+
// A retained document may load after its authored spec was replaced. It can
|
|
70
|
+
// still acknowledge paint, but cannot own or restore the new spec's drafts.
|
|
71
|
+
const current = node.data.specVersion === authored.data.specVersion && node.data.url === authored.data.url;
|
|
72
|
+
const snapshot: Snapshot | undefined = current
|
|
73
|
+
? (snapshots.get(nodeId) ?? {
|
|
74
|
+
epoch: canvasInteractionEpoch.value,
|
|
75
|
+
version: authored.data.specVersion,
|
|
76
|
+
documentUrl: authored.data.url,
|
|
77
|
+
state: null,
|
|
78
|
+
owners: [],
|
|
79
|
+
})
|
|
80
|
+
: undefined;
|
|
81
|
+
if (snapshot) snapshots.set(nodeId, snapshot);
|
|
85
82
|
const ready = new Promise<boolean | 'cancelled'>((resolve) => {
|
|
86
83
|
const finish = (loaded: boolean | 'cancelled') => {
|
|
87
84
|
clearTimeout(timer);
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
window.removeEventListener('message', receive);
|
|
86
|
+
signal?.removeEventListener('abort', cancel);
|
|
87
|
+
if (loaded !== true && snapshot) snapshot.owners = snapshot.owners.filter((entry) => entry !== owner);
|
|
90
88
|
resolve(loaded);
|
|
91
89
|
};
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
const cancel = () => finish('cancelled');
|
|
91
|
+
const receive = (event: MessageEvent) => {
|
|
92
|
+
if (
|
|
93
|
+
event.source !== source ||
|
|
94
|
+
event.data?.source !== JSON_VIEWER_STATE_SOURCE ||
|
|
95
|
+
event.data?.token !== token ||
|
|
96
|
+
event.data?.nodeId !== nodeId
|
|
97
|
+
)
|
|
98
|
+
return;
|
|
99
|
+
if (event.data.type === 'mounted') clearTimeout(timer);
|
|
100
|
+
if (event.data.type === 'ready') finish(true);
|
|
98
101
|
};
|
|
102
|
+
const owner = { token, source };
|
|
99
103
|
// A missing runtime fails, but a healthy offscreen document can wait for
|
|
100
104
|
// paint indefinitely. Its synchronous mount acknowledgement ends the deadline;
|
|
101
105
|
// only the later frame-gated ready message may promote it.
|
|
102
|
-
const timer = setTimeout(() => finish(false),
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
const timer = setTimeout(() => finish(false), VIEWER_LOAD_DEADLINE_MS);
|
|
107
|
+
if (snapshot) {
|
|
108
|
+
snapshot.owners = snapshot.owners.filter((entry) => entry.source !== source).slice(-1);
|
|
109
|
+
snapshot.owners.push(owner);
|
|
110
|
+
}
|
|
111
|
+
window.addEventListener('message', receive);
|
|
112
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
107
113
|
});
|
|
108
|
-
source.postMessage({ source: JSON_VIEWER_STATE_SOURCE, type: 'restore', token, state: snapshot
|
|
114
|
+
source.postMessage({ source: JSON_VIEWER_STATE_SOURCE, type: 'restore', token, state: snapshot?.state ?? null }, '*');
|
|
109
115
|
return ready;
|
|
110
116
|
}
|
|
@@ -637,18 +637,22 @@ export function resizeNode(id: string, size: { width: number; height: number }):
|
|
|
637
637
|
updateNode(id, { size });
|
|
638
638
|
}
|
|
639
639
|
|
|
640
|
-
export function bringToFront(id: string): void {
|
|
640
|
+
export function bringToFront(id: string, persist = true): void {
|
|
641
641
|
const existing = nodes.value.get(id);
|
|
642
642
|
if (!existing) return;
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
if (typeof childId === 'string' && nodes.value.has(childId)) updateNode(childId, { zIndex: maxZ++ });
|
|
649
|
-
}
|
|
643
|
+
const updates = [{ id, zIndex: maxZ++ }];
|
|
644
|
+
// A group is a frame: it must never stack above its own children.
|
|
645
|
+
if (existing.type === 'group' && Array.isArray(existing.data.children)) {
|
|
646
|
+
for (const childId of existing.data.children) {
|
|
647
|
+
if (typeof childId === 'string' && nodes.value.has(childId)) updates.push({ id: childId, zIndex: maxZ++ });
|
|
650
648
|
}
|
|
649
|
+
}
|
|
650
|
+
batch(() => {
|
|
651
|
+
for (const update of updates) updateNode(update.id, { zIndex: update.zIndex });
|
|
651
652
|
});
|
|
653
|
+
// Title-bar gestures commit stacking with geometry at pointer-up. A separate
|
|
654
|
+
// drag-start write would broadcast an old layout over the in-progress move.
|
|
655
|
+
if (persist) void pushCanvasUpdate(updates, { recordHistory: false });
|
|
652
656
|
activeNodeId.value = id;
|
|
653
657
|
}
|
|
654
658
|
|
|
@@ -820,14 +824,17 @@ export function cancelViewportAnimation(): void {
|
|
|
820
824
|
// ── Persistence ───────────────────────────────────────────────
|
|
821
825
|
const STORAGE_KEY = 'pmx-canvas-layout';
|
|
822
826
|
|
|
823
|
-
export function persistLayout(options: { recordHistory?: boolean } = {}): void {
|
|
827
|
+
export function persistLayout(options: { recordHistory?: boolean; raisedNodeId?: string } = {}): void {
|
|
824
828
|
try {
|
|
825
829
|
const allNodes = Array.from(nodes.value.values());
|
|
830
|
+
const raised = options.raisedNodeId ? nodes.value.get(options.raisedNodeId) : undefined;
|
|
831
|
+
const raisedChildren = raised?.type === 'group' && Array.isArray(raised.data.children) ? raised.data.children : [];
|
|
826
832
|
const nodeUpdates = allNodes.map((n) => ({
|
|
827
833
|
id: n.id,
|
|
828
834
|
position: n.position,
|
|
829
835
|
size: n.size,
|
|
830
836
|
collapsed: n.collapsed,
|
|
837
|
+
...(n.id === raised?.id || raisedChildren.includes(n.id) ? { zIndex: n.zIndex } : {}),
|
|
831
838
|
}));
|
|
832
839
|
const layout = {
|
|
833
840
|
viewport: viewport.value,
|
|
@@ -931,7 +938,7 @@ export function fitAll(containerW: number, containerH: number): void {
|
|
|
931
938
|
}
|
|
932
939
|
|
|
933
940
|
// ── Focus node ────────────────────────────────────────────────
|
|
934
|
-
export function focusNode(id: string, options: { recordHistory?: boolean } = {}): void {
|
|
941
|
+
export function focusNode(id: string, options: { recordHistory?: boolean; persistStacking?: boolean } = {}): void {
|
|
935
942
|
const node = nodes.value.get(id);
|
|
936
943
|
if (!node) return;
|
|
937
944
|
const v = viewport.value;
|
|
@@ -959,7 +966,7 @@ export function focusNode(id: string, options: { recordHistory?: boolean } = {})
|
|
|
959
966
|
300,
|
|
960
967
|
options,
|
|
961
968
|
);
|
|
962
|
-
bringToFront(id);
|
|
969
|
+
bringToFront(id, options.persistStacking !== false);
|
|
963
970
|
}
|
|
964
971
|
|
|
965
972
|
// ── Cycle focus ───────────────────────────────────────────────
|
|
@@ -969,7 +976,6 @@ export function cycleActiveNode(direction: 1 | -1 = 1): void {
|
|
|
969
976
|
const currentIdx = activeNodeId.value ? all.indexOf(activeNodeId.value) : -1;
|
|
970
977
|
const nextIdx = (currentIdx + direction + all.length) % all.length;
|
|
971
978
|
const nextId = all[nextIdx];
|
|
972
|
-
bringToFront(nextId);
|
|
973
979
|
focusNode(nextId);
|
|
974
980
|
}
|
|
975
981
|
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { signal } from '@preact/signals';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
interpolateCamera,
|
|
4
|
+
resolveStop,
|
|
5
|
+
tourStopSchema,
|
|
6
|
+
type TourPresenter,
|
|
7
|
+
type TourStop,
|
|
8
|
+
} from '../../shared/tour.js';
|
|
3
9
|
import { canvasArea } from '../canvas/canvas-area';
|
|
4
10
|
import type { ViewportState } from '../types';
|
|
5
|
-
import {
|
|
11
|
+
import { boardGeneration, catalogEpoch } from './boards-store';
|
|
12
|
+
import { animateCameraPath, cancelViewportAnimation, nodes, viewport } from './canvas-store';
|
|
6
13
|
import { requestBestEffort, requestJson } from './intent-bridge';
|
|
7
14
|
|
|
8
15
|
export const presenting = signal(false);
|
|
9
16
|
export const recordingCamera = signal(false);
|
|
10
17
|
/** Why the current stop cannot be shown (missing target, no stops, server unreachable). */
|
|
11
18
|
export const presentationError = signal('');
|
|
19
|
+
/** Read-only mirror of the server's shared cursor, never advanced optimistically. */
|
|
20
|
+
export const presentationCursor = signal<{ index: number; total: number } | null>(null);
|
|
12
21
|
|
|
13
22
|
export function ownsCamera(): boolean {
|
|
14
23
|
return presenting.value || recordingCamera.value;
|
|
@@ -24,7 +33,21 @@ function prefersReducedMotion(): boolean {
|
|
|
24
33
|
*/
|
|
25
34
|
export function playStop(stop: TourStop, onDone?: (end: ViewportState) => void): void {
|
|
26
35
|
const area = canvasArea();
|
|
27
|
-
|
|
36
|
+
let target: ViewportState;
|
|
37
|
+
if (presenting.value && !recordingCamera.value && !('viewport' in stop.target)) {
|
|
38
|
+
const padding = stop.padding ?? 40;
|
|
39
|
+
const controls = document.querySelector('.presentation-controls')?.getBoundingClientRect();
|
|
40
|
+
const top = Math.max(padding, controls ? controls.bottom - area.top + 12 : 0);
|
|
41
|
+
target = resolveStop(
|
|
42
|
+
{ ...stop, padding: 0 },
|
|
43
|
+
[...nodes.value.values()],
|
|
44
|
+
area.width - 2 * padding,
|
|
45
|
+
area.height - top - padding,
|
|
46
|
+
);
|
|
47
|
+
target = { ...target, x: target.x + padding, y: target.y + top };
|
|
48
|
+
} else {
|
|
49
|
+
target = resolveStop(stop, [...nodes.value.values()], area.width, area.height);
|
|
50
|
+
}
|
|
28
51
|
const from = { ...viewport.value };
|
|
29
52
|
const duration = prefersReducedMotion() ? 0 : (stop.duration ?? 1) * 1000;
|
|
30
53
|
animateCameraPath(
|
|
@@ -35,6 +58,7 @@ export function playStop(stop: TourStop, onDone?: (end: ViewportState) => void):
|
|
|
35
58
|
}
|
|
36
59
|
|
|
37
60
|
let stepSeq = 0;
|
|
61
|
+
let presentationRun = 0;
|
|
38
62
|
|
|
39
63
|
/**
|
|
40
64
|
* Show a tour stop, entering presentation if needed. Entering hides the
|
|
@@ -58,41 +82,137 @@ export function presentStop(stop: TourStop): void {
|
|
|
58
82
|
else play();
|
|
59
83
|
}
|
|
60
84
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
85
|
+
let requestSeq = 0;
|
|
86
|
+
const presenterId = crypto.randomUUID();
|
|
87
|
+
let lastDelivery: { catalogEpoch: string; sequence: number; applied: boolean } | null = null;
|
|
88
|
+
|
|
89
|
+
function nextPresenter(): TourPresenter | null {
|
|
90
|
+
if (catalogEpoch === null || boardGeneration === null) {
|
|
91
|
+
presentationError.value = 'The board is still connecting. Try presenting again when it is ready.';
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return { clientId: presenterId, intent: ++requestSeq, catalogEpoch, boardGeneration };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isCurrentPresenter(presenter: TourPresenter): boolean {
|
|
98
|
+
return (
|
|
99
|
+
presenter.intent === requestSeq &&
|
|
100
|
+
presenter.catalogEpoch === catalogEpoch &&
|
|
101
|
+
presenter.boardGeneration === boardGeneration
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function acceptDelivery(data: Record<string, unknown>, apply: boolean): boolean {
|
|
106
|
+
if (
|
|
107
|
+
typeof data.catalogEpoch !== 'string' ||
|
|
108
|
+
data.catalogEpoch !== catalogEpoch ||
|
|
109
|
+
data.boardGeneration !== boardGeneration ||
|
|
110
|
+
typeof data.sequence !== 'number' ||
|
|
111
|
+
(lastDelivery?.catalogEpoch === catalogEpoch &&
|
|
112
|
+
(data.sequence < lastDelivery.sequence ||
|
|
113
|
+
(data.sequence === lastDelivery.sequence && (!apply || lastDelivery.applied))))
|
|
114
|
+
)
|
|
115
|
+
return false;
|
|
116
|
+
lastDelivery = { catalogEpoch: data.catalogEpoch, sequence: data.sequence, applied: apply };
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Apply either delivery of the server's shared cursor, without replaying its echo. */
|
|
121
|
+
export function applyTourStep(data: Record<string, unknown>): void {
|
|
122
|
+
if (recordingCamera.value) return;
|
|
123
|
+
const stop = tourStopSchema.safeParse(data.stop);
|
|
124
|
+
const display = presenting.value || data.present === true;
|
|
125
|
+
// Observe hidden steps too. The same operation's HTTP acknowledgement may
|
|
126
|
+
// authorize local presentation later, but an older acknowledgement must not.
|
|
127
|
+
if (!stop.success || !acceptDelivery(data, display) || !display) return;
|
|
128
|
+
if (typeof data.index === 'number' && typeof data.total === 'number') {
|
|
129
|
+
presentationCursor.value = { index: data.index, total: data.total };
|
|
130
|
+
}
|
|
131
|
+
presentStop(stop.data);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function applyTourExit(data: Record<string, unknown>): void {
|
|
135
|
+
if (!recordingCamera.value && acceptDelivery(data, true)) hidePresentation();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Move the shared cursor. A successful response also works while events are delayed or disconnected. */
|
|
139
|
+
export async function stepTour(
|
|
140
|
+
to: { stop: number } | { step: 'next' | 'previous' },
|
|
141
|
+
presenter = nextPresenter(),
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
if (!presenter) return;
|
|
144
|
+
const run = presentationRun;
|
|
145
|
+
const previous = lastDelivery;
|
|
146
|
+
const result = await requestJson<
|
|
147
|
+
| {
|
|
148
|
+
ok: true;
|
|
149
|
+
catalogEpoch: string;
|
|
150
|
+
boardGeneration: number;
|
|
151
|
+
sequence: number;
|
|
152
|
+
index: number;
|
|
153
|
+
total: number;
|
|
154
|
+
stop: TourStop;
|
|
155
|
+
}
|
|
156
|
+
| { ok: true; superseded: true }
|
|
157
|
+
| { ok: false; error?: string }
|
|
158
|
+
>(
|
|
68
159
|
'step tour',
|
|
69
160
|
'/api/canvas/tour/go',
|
|
70
161
|
{ ok: false, error: 'Could not reach the canvas server.' },
|
|
71
162
|
{
|
|
72
163
|
method: 'POST',
|
|
73
164
|
headers: { 'Content-Type': 'application/json' },
|
|
74
|
-
body: JSON.stringify({ ...to, present: false }),
|
|
165
|
+
body: JSON.stringify({ ...to, present: false, presenter }),
|
|
75
166
|
},
|
|
76
167
|
);
|
|
77
|
-
|
|
168
|
+
// Only a local cancellation invalidates this intent. Remote steps/exits are
|
|
169
|
+
// ordered by the server stamp, even if an older exit hid the pending restart.
|
|
170
|
+
if (run !== presentationRun || !isCurrentPresenter(presenter)) return;
|
|
171
|
+
if (result.ok) {
|
|
172
|
+
if ('superseded' in result) {
|
|
173
|
+
if (previous === lastDelivery)
|
|
174
|
+
presentationError.value = 'The presentation request was superseded. Exit and present again.';
|
|
175
|
+
} else applyTourStep({ ...result, present: true });
|
|
176
|
+
} else if (previous === lastDelivery) presentationError.value = result.error ?? 'Could not change the tour stop.';
|
|
78
177
|
}
|
|
79
178
|
|
|
80
179
|
/** Present from the first stop, or resume where the tour already is (`?present=1`). */
|
|
81
180
|
export async function startPresentation(resume = false): Promise<void> {
|
|
181
|
+
const run = ++presentationRun;
|
|
82
182
|
presenting.value = true;
|
|
83
183
|
presentationError.value = '';
|
|
184
|
+
presentationCursor.value = null;
|
|
185
|
+
// Reserve the gesture before the read: an older resume cannot overtake Next.
|
|
186
|
+
const presenter = nextPresenter();
|
|
187
|
+
if (!presenter) return;
|
|
84
188
|
const at = resume
|
|
85
189
|
? (await requestJson<{ position?: number | null }>('read tour', '/api/canvas/tour', {})).position
|
|
86
190
|
: null;
|
|
87
|
-
|
|
191
|
+
if (run !== presentationRun || !isCurrentPresenter(presenter)) return;
|
|
192
|
+
await stepTour({ stop: typeof at === 'number' ? at : 0 }, presenter);
|
|
88
193
|
}
|
|
89
194
|
|
|
90
|
-
|
|
195
|
+
function hidePresentation(): void {
|
|
196
|
+
stepSeq++;
|
|
197
|
+
cancelViewportAnimation();
|
|
91
198
|
presenting.value = false;
|
|
92
199
|
presentationError.value = '';
|
|
200
|
+
presentationCursor.value = null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Local cancellation also fences pending requests from this presentation. */
|
|
204
|
+
export function dismissPresentation(): void {
|
|
205
|
+
presentationRun++;
|
|
206
|
+
hidePresentation();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function endPresentation(): void {
|
|
210
|
+
dismissPresentation();
|
|
211
|
+
const presenter = nextPresenter();
|
|
212
|
+
if (!presenter) return;
|
|
93
213
|
void requestBestEffort('exit tour', '/api/canvas/tour/exit', {
|
|
94
214
|
method: 'POST',
|
|
95
215
|
headers: { 'Content-Type': 'application/json' },
|
|
96
|
-
body:
|
|
216
|
+
body: JSON.stringify({ presenter }),
|
|
97
217
|
});
|
|
98
218
|
}
|