pmx-canvas 0.7.0 → 0.7.1
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/extension.mjs +72 -11
- package/CHANGELOG.md +16 -0
- package/Readme.md +10 -8
- package/dist/canvas/global.css +73 -10
- package/dist/canvas/index.js +58 -58
- package/dist/types/cli/daemon.d.ts +5 -5
- package/dist/types/cli/shared.d.ts +3 -1
- package/docs/cli.md +5 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +17 -9
- package/skills/pmx-canvas/evals/evals.json +130 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/skills/pmx-canvas/references/tours-and-recording.md +69 -0
- package/skills/pmx-canvas-testing/SKILL.md +7 -2
- package/src/cli/commands/apps.ts +1 -1
- package/src/cli/commands/export.ts +9 -2
- package/src/cli/daemon.ts +37 -7
- package/src/cli/shared.ts +11 -2
- package/src/client/App.tsx +0 -1
- package/src/client/canvas/AttentionToast.tsx +5 -3
- package/src/client/canvas/CanvasNode.tsx +0 -1
- package/src/client/canvas/ContextMenu.tsx +0 -1
- package/src/client/canvas/ExportDialog.tsx +3 -1
- package/src/client/canvas/Presentation.tsx +39 -9
- package/src/client/canvas/SelectionBar.tsx +0 -2
- package/src/client/nodes/AttachmentNode.tsx +27 -30
- package/src/client/state/attention-bridge.ts +3 -1
- package/src/client/state/sse-bridge.ts +3 -1
- package/src/client/theme/global.css +73 -10
- package/src/server/operations/ops/imports.ts +19 -3
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { useEffect, useState } from 'preact/hooks';
|
|
1
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { cameraSchema, interpolateCamera, resolveStop, type Camera, type Tour } from '../../shared/tour.js';
|
|
3
3
|
import { canvasArea } from './canvas-area';
|
|
4
4
|
import { cancelViewportAnimation, hasInitialServerLayout, nodes, viewport } from '../state/canvas-store';
|
|
5
5
|
import { presenting, recordingCamera } from '../state/presentation';
|
|
6
6
|
import { requestJson } from '../state/intent-bridge';
|
|
7
|
+
import { usePanZoom } from './use-pan-zoom';
|
|
7
8
|
|
|
8
9
|
declare global {
|
|
9
10
|
interface Window {
|
|
@@ -22,6 +23,17 @@ export function Presentation() {
|
|
|
22
23
|
const [error, setError] = useState('');
|
|
23
24
|
const active = presenting.value;
|
|
24
25
|
const ready = hasInitialServerLayout.value;
|
|
26
|
+
const frame = useRef(0);
|
|
27
|
+
const moveCamera = (camera: Camera) => {
|
|
28
|
+
cancelAnimationFrame(frame.current);
|
|
29
|
+
viewport.value = camera;
|
|
30
|
+
};
|
|
31
|
+
// A separate gesture surface keeps cards read-only and camera changes local.
|
|
32
|
+
const navigationRef = usePanZoom({
|
|
33
|
+
viewport,
|
|
34
|
+
onViewportChange: moveCamera,
|
|
35
|
+
onViewportCommit: moveCamera,
|
|
36
|
+
});
|
|
25
37
|
useEffect(() => {
|
|
26
38
|
const params = new URLSearchParams(location.search);
|
|
27
39
|
presenting.value = params.get('present') === '1';
|
|
@@ -76,20 +88,20 @@ export function Presentation() {
|
|
|
76
88
|
const from = { ...viewport.value };
|
|
77
89
|
const start = performance.now();
|
|
78
90
|
const duration = (stop.duration ?? 1) * 1000;
|
|
79
|
-
let frame = 0;
|
|
80
91
|
const tick = (now: number) => {
|
|
81
92
|
if (recordingCamera.value) return;
|
|
82
93
|
const t = duration === 0 ? 1 : Math.min(1, (now - start) / duration);
|
|
83
94
|
viewport.value = interpolateCamera(from, target, t, area.width, area.height, stop.easing, stop.pullback);
|
|
84
|
-
if (t < 1) frame = requestAnimationFrame(tick);
|
|
95
|
+
if (t < 1) frame.current = requestAnimationFrame(tick);
|
|
85
96
|
};
|
|
86
|
-
frame = requestAnimationFrame(tick);
|
|
87
|
-
return () => cancelAnimationFrame(frame);
|
|
97
|
+
frame.current = requestAnimationFrame(tick);
|
|
98
|
+
return () => cancelAnimationFrame(frame.current);
|
|
88
99
|
}, [active, index, tour]);
|
|
89
100
|
useEffect(() => {
|
|
90
101
|
if (!active) return;
|
|
91
102
|
const key = (event: KeyboardEvent) => {
|
|
92
103
|
event.stopImmediatePropagation();
|
|
104
|
+
if (event.target instanceof HTMLButtonElement && ['Enter', ' '].includes(event.key)) return;
|
|
93
105
|
if (['Escape', 'ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp', ' '].includes(event.key))
|
|
94
106
|
event.preventDefault();
|
|
95
107
|
if (event.key === 'Escape') presenting.value = false;
|
|
@@ -100,9 +112,27 @@ export function Presentation() {
|
|
|
100
112
|
document.addEventListener('keydown', key, true);
|
|
101
113
|
return () => document.removeEventListener('keydown', key, true);
|
|
102
114
|
}, [active, tour]);
|
|
103
|
-
return active
|
|
104
|
-
|
|
105
|
-
{
|
|
106
|
-
|
|
115
|
+
return active ? (
|
|
116
|
+
<>
|
|
117
|
+
{!recordingCamera.value && (
|
|
118
|
+
<div
|
|
119
|
+
class="presentation-navigation"
|
|
120
|
+
ref={navigationRef}
|
|
121
|
+
onPointerDown={(event) => {
|
|
122
|
+
if (event.button === 1 || event.button === 2) cancelAnimationFrame(frame.current);
|
|
123
|
+
}}
|
|
124
|
+
onContextMenu={(event) => event.preventDefault()}
|
|
125
|
+
>
|
|
126
|
+
<button type="button" class="presentation-exit" onClick={() => (presenting.value = false)}>
|
|
127
|
+
Exit presentation
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
{error && (
|
|
132
|
+
<div class="presentation-error" role="alert">
|
|
133
|
+
{error}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</>
|
|
107
137
|
) : null;
|
|
108
138
|
}
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
clearSelection,
|
|
9
9
|
contextPinnedNodeIds,
|
|
10
10
|
distributeSelection,
|
|
11
|
-
removeNode,
|
|
12
11
|
selectedNodeIds,
|
|
13
12
|
} from '../state/canvas-store';
|
|
14
13
|
import { createEdgeFromClient, createGroupFromClient, removeNodeFromClient } from '../state/intent-bridge';
|
|
@@ -51,7 +50,6 @@ export function SelectionBar() {
|
|
|
51
50
|
const handleDelete = useCallback(() => {
|
|
52
51
|
const ids = Array.from(selectedNodeIds.value);
|
|
53
52
|
for (const id of ids) {
|
|
54
|
-
removeNode(id);
|
|
55
53
|
void removeNodeFromClient(id);
|
|
56
54
|
}
|
|
57
55
|
clearSelection();
|
|
@@ -11,7 +11,6 @@ export function AttachmentNode({ node, expanded = false }: { node: CanvasNodeSta
|
|
|
11
11
|
const boardId = activeBoardId.value;
|
|
12
12
|
const [job, setJob] = useState<DocumentImport | null>(null);
|
|
13
13
|
const [review, setReview] = useState(expanded);
|
|
14
|
-
const [consent, setConsent] = useState(false);
|
|
15
14
|
const [busy, setBusy] = useState(false);
|
|
16
15
|
useEffect(() => {
|
|
17
16
|
let current = true;
|
|
@@ -53,7 +52,6 @@ export function AttachmentNode({ node, expanded = false }: { node: CanvasNodeSta
|
|
|
53
52
|
setBusy(false);
|
|
54
53
|
if (result?.import) {
|
|
55
54
|
setJob(result.import);
|
|
56
|
-
setConsent(false);
|
|
57
55
|
setReview(false);
|
|
58
56
|
if (expanded && action === 'commit') collapseExpandedNode();
|
|
59
57
|
}
|
|
@@ -74,22 +72,19 @@ export function AttachmentNode({ node, expanded = false }: { node: CanvasNodeSta
|
|
|
74
72
|
Download original · {Math.ceil(Number(node.data.size ?? 0) / 1024)} KB
|
|
75
73
|
</a>
|
|
76
74
|
{job?.reason && <p>{job.reason}</p>}
|
|
77
|
-
{canRequest &&
|
|
78
|
-
<button type="button" class="home-board-action" onClick={() => setConsent(true)}>
|
|
79
|
-
Ask agent to import
|
|
80
|
-
</button>
|
|
81
|
-
)}
|
|
82
|
-
{consent && (
|
|
75
|
+
{canRequest && (
|
|
83
76
|
<>
|
|
84
|
-
<p>
|
|
85
|
-
|
|
86
|
-
original. Canvas does not convert files itself.
|
|
77
|
+
<p id={`import-consent-${node.id}`}>
|
|
78
|
+
Asking the agent to import allows it to process this file with its tools and model provider.
|
|
87
79
|
</p>
|
|
88
|
-
<button
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
class="attachment-button is-primary"
|
|
83
|
+
aria-describedby={`import-consent-${node.id}`}
|
|
84
|
+
disabled={busy}
|
|
85
|
+
onClick={() => void act('request')}
|
|
86
|
+
>
|
|
87
|
+
Ask agent to import
|
|
93
88
|
</button>
|
|
94
89
|
</>
|
|
95
90
|
)}
|
|
@@ -98,17 +93,17 @@ export function AttachmentNode({ node, expanded = false }: { node: CanvasNodeSta
|
|
|
98
93
|
No automatic extraction. Ask your connected agent to check its pending steering or use canvas_import list.
|
|
99
94
|
</p>
|
|
100
95
|
)}
|
|
101
|
-
{job?.status === 'drafted' && (
|
|
96
|
+
{job?.status === 'drafted' && !review && (
|
|
102
97
|
<button
|
|
103
98
|
type="button"
|
|
104
|
-
class="
|
|
99
|
+
class="attachment-button is-primary"
|
|
105
100
|
onClick={() => (expanded ? setReview(true) : expandNode(node.id))}
|
|
106
101
|
>
|
|
107
102
|
Review Markdown · {job.sections.length} {job.sections.length === 1 ? 'card' : 'cards'}
|
|
108
103
|
</button>
|
|
109
104
|
)}
|
|
110
105
|
{job && ['requested', 'drafted'].includes(job.status) && (
|
|
111
|
-
<button type="button" class="
|
|
106
|
+
<button type="button" class="attachment-button" disabled={busy} onClick={() => void act('cancel')}>
|
|
112
107
|
Cancel import
|
|
113
108
|
</button>
|
|
114
109
|
)}
|
|
@@ -130,17 +125,19 @@ export function AttachmentNode({ node, expanded = false }: { node: CanvasNodeSta
|
|
|
130
125
|
<pre>{section.markdown}</pre>
|
|
131
126
|
</section>
|
|
132
127
|
))}
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
128
|
+
<div class="attachment-review-actions">
|
|
129
|
+
<button type="button" class="attachment-button" onClick={() => setReview(false)}>
|
|
130
|
+
Close review
|
|
131
|
+
</button>
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
class="attachment-button is-primary"
|
|
135
|
+
disabled={busy || boardId !== job.boardId}
|
|
136
|
+
onClick={() => void act('commit')}
|
|
137
|
+
>
|
|
138
|
+
Add to board
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
144
141
|
</div>
|
|
145
142
|
)}
|
|
146
143
|
</div>
|
|
@@ -274,7 +274,9 @@ function enqueueToast(entry: AttentionEntry): void {
|
|
|
274
274
|
|
|
275
275
|
/** Show a transient toast from arbitrary client code (e.g. AX action feedback). */
|
|
276
276
|
export function showToast(tone: AttentionTone, title: string, detail = '', nodeIds: string[] = []): void {
|
|
277
|
-
|
|
277
|
+
const entry = makeEntry(tone, title, detail, nodeIds);
|
|
278
|
+
pushAttentionHistory(entry);
|
|
279
|
+
enqueueToast(entry);
|
|
278
280
|
}
|
|
279
281
|
|
|
280
282
|
function pulseNodes(nodeIds: string[]): void {
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
import { agentActivity, applyPresenceSnapshot, sessionActive } from './presence-store';
|
|
42
42
|
import { applyHumanSnapshot, startHumanPresence } from './human-store';
|
|
43
43
|
import type { HumanPresenceSnapshot } from '../../shared/human-presence.js';
|
|
44
|
-
import { applySessionReceipt, refreshTimeline } from './session-store';
|
|
44
|
+
import { applySessionReceipt, dismissSessionReceipt, refreshTimeline } from './session-store';
|
|
45
45
|
import { initSessionThemeOverride, themeOverrideActive } from './theme-override';
|
|
46
46
|
import { DEFAULT_POSITIONS, makeNodeState } from './node-factory';
|
|
47
47
|
import { invalidateTokenCache } from '../theme/tokens';
|
|
@@ -479,6 +479,8 @@ function resyncForBoardSwitch(): void {
|
|
|
479
479
|
hasInitialServerLayout.value = false;
|
|
480
480
|
resetIntents();
|
|
481
481
|
resetCanvasInteractionState();
|
|
482
|
+
resetAttentionBridge();
|
|
483
|
+
dismissSessionReceipt();
|
|
482
484
|
axSurfaceState.value = null;
|
|
483
485
|
void refreshAxSurface();
|
|
484
486
|
const layoutAtRequest = layoutRevision;
|
|
@@ -4784,7 +4784,7 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
4784
4784
|
position: absolute;
|
|
4785
4785
|
top: 18px;
|
|
4786
4786
|
right: 18px;
|
|
4787
|
-
max-width: min(
|
|
4787
|
+
max-width: min(360px, calc(100% - 36px));
|
|
4788
4788
|
display: inline-flex;
|
|
4789
4789
|
align-items: center;
|
|
4790
4790
|
gap: 8px;
|
|
@@ -4792,7 +4792,7 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
4792
4792
|
background: color-mix(in srgb, var(--c-panel-glass) 96%, transparent);
|
|
4793
4793
|
backdrop-filter: blur(14px);
|
|
4794
4794
|
border: 1px solid color-mix(in srgb, var(--c-line) 76%, var(--c-warn) 24%);
|
|
4795
|
-
border-radius:
|
|
4795
|
+
border-radius: 12px;
|
|
4796
4796
|
box-shadow: 0 8px 22px var(--c-shadow);
|
|
4797
4797
|
color: var(--c-text);
|
|
4798
4798
|
font: inherit;
|
|
@@ -4831,15 +4831,27 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
4831
4831
|
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 28%, transparent);
|
|
4832
4832
|
}
|
|
4833
4833
|
|
|
4834
|
+
.attention-toast-copy {
|
|
4835
|
+
display: grid;
|
|
4836
|
+
gap: 4px;
|
|
4837
|
+
min-width: 0;
|
|
4838
|
+
text-align: left;
|
|
4839
|
+
overflow-wrap: anywhere;
|
|
4840
|
+
}
|
|
4841
|
+
|
|
4842
|
+
.attention-toast-detail {
|
|
4843
|
+
color: var(--c-text-soft);
|
|
4844
|
+
font-size: 12px;
|
|
4845
|
+
line-height: 1.45;
|
|
4846
|
+
max-height: 9em;
|
|
4847
|
+
overflow: auto;
|
|
4848
|
+
}
|
|
4849
|
+
|
|
4834
4850
|
.attention-toast-title {
|
|
4835
4851
|
font-size: 12px;
|
|
4836
4852
|
font-weight: 600;
|
|
4837
4853
|
line-height: 1.2;
|
|
4838
4854
|
letter-spacing: -0.005em;
|
|
4839
|
-
white-space: nowrap;
|
|
4840
|
-
overflow: hidden;
|
|
4841
|
-
text-overflow: ellipsis;
|
|
4842
|
-
max-width: 200px;
|
|
4843
4855
|
}
|
|
4844
4856
|
|
|
4845
4857
|
.attention-history {
|
|
@@ -6841,12 +6853,23 @@ html.is-node-dragging .intent-line-layer {
|
|
|
6841
6853
|
padding: 4px 12px;
|
|
6842
6854
|
cursor: pointer;
|
|
6843
6855
|
}
|
|
6844
|
-
.is-presenting > :not(.app-main):not(.presentation-error),
|
|
6856
|
+
.is-presenting > :not(.app-main):not(.presentation-error):not(.presentation-navigation),
|
|
6845
6857
|
.is-presenting .app-main > :not(.canvas-region),
|
|
6846
6858
|
.is-presenting .canvas-region > :not(.canvas-viewport) {
|
|
6847
6859
|
display: none !important;
|
|
6848
6860
|
}
|
|
6849
6861
|
.is-presenting .canvas-viewport { pointer-events: none; }
|
|
6862
|
+
.presentation-navigation {
|
|
6863
|
+
position: fixed;
|
|
6864
|
+
inset: 0;
|
|
6865
|
+
z-index: 99;
|
|
6866
|
+
}
|
|
6867
|
+
.presentation-exit {
|
|
6868
|
+
position: absolute;
|
|
6869
|
+
top: 16px;
|
|
6870
|
+
right: 16px;
|
|
6871
|
+
box-shadow: 0 2px 12px var(--c-shadow);
|
|
6872
|
+
}
|
|
6850
6873
|
.presentation-error {
|
|
6851
6874
|
position: fixed;
|
|
6852
6875
|
bottom: 16px;
|
|
@@ -7104,10 +7127,17 @@ html.is-node-dragging .intent-line-layer {
|
|
|
7104
7127
|
}
|
|
7105
7128
|
.export-dialog-links {
|
|
7106
7129
|
display: flex;
|
|
7130
|
+
flex-wrap: wrap;
|
|
7107
7131
|
gap: 12px;
|
|
7132
|
+
margin-top: 12px;
|
|
7108
7133
|
}
|
|
7109
7134
|
.export-dialog-links a {
|
|
7110
|
-
|
|
7135
|
+
display: inline-flex;
|
|
7136
|
+
align-items: center;
|
|
7137
|
+
justify-content: center;
|
|
7138
|
+
box-sizing: border-box;
|
|
7139
|
+
text-decoration: none;
|
|
7140
|
+
word-break: normal;
|
|
7111
7141
|
}
|
|
7112
7142
|
|
|
7113
7143
|
/* ── Home categories ── */
|
|
@@ -7267,9 +7297,42 @@ html.is-node-dragging .intent-line-layer {
|
|
|
7267
7297
|
.attachment-node p { margin: 0; color: var(--c-text-soft); line-height: 1.5; }
|
|
7268
7298
|
.attachment-node a, .md-import-source a { color: var(--c-accent); }
|
|
7269
7299
|
.attachment-status { font-weight: 600; }
|
|
7270
|
-
.attachment-
|
|
7271
|
-
.
|
|
7300
|
+
.attachment-button,
|
|
7301
|
+
.presentation-exit,
|
|
7302
|
+
.export-dialog-links a {
|
|
7303
|
+
min-height: 38px;
|
|
7304
|
+
padding: 8px 14px;
|
|
7305
|
+
border: 1px solid var(--c-text-soft);
|
|
7306
|
+
border-radius: 8px;
|
|
7307
|
+
background: var(--c-panel);
|
|
7308
|
+
color: var(--c-text);
|
|
7309
|
+
font: inherit;
|
|
7310
|
+
font-weight: 600;
|
|
7311
|
+
cursor: pointer;
|
|
7312
|
+
}
|
|
7313
|
+
.attachment-button.is-primary,
|
|
7314
|
+
.export-dialog-links a.is-primary { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-bg); }
|
|
7315
|
+
.attachment-button:hover:not(:disabled),
|
|
7316
|
+
.presentation-exit:hover,
|
|
7317
|
+
.export-dialog-links a:hover { filter: brightness(1.12); }
|
|
7318
|
+
.attachment-button:focus-visible,
|
|
7319
|
+
.presentation-exit:focus-visible,
|
|
7320
|
+
.export-dialog-links a:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 3px; }
|
|
7321
|
+
.attachment-button:disabled { opacity: 0.5; cursor: default; }
|
|
7272
7322
|
.attachment-review { width: 100%; border-top: 1px solid var(--c-line); padding-top: 12px; }
|
|
7323
|
+
.attachment-review-actions {
|
|
7324
|
+
position: sticky;
|
|
7325
|
+
bottom: -16px;
|
|
7326
|
+
display: flex;
|
|
7327
|
+
flex-wrap: wrap;
|
|
7328
|
+
justify-content: flex-end;
|
|
7329
|
+
gap: 12px;
|
|
7330
|
+
margin-top: 24px;
|
|
7331
|
+
padding: 16px 0;
|
|
7332
|
+
border-top: 1px solid var(--c-line);
|
|
7333
|
+
background: var(--c-panel);
|
|
7334
|
+
z-index: 1;
|
|
7335
|
+
}
|
|
7273
7336
|
.attachment-review pre { white-space: pre-wrap; overflow-wrap: anywhere; font: 12px/1.6 var(--mono); padding: 12px; background: var(--c-bg); border-radius: 8px; }
|
|
7274
7337
|
.attachment-review h4 { margin-bottom: 6px; }
|
|
7275
7338
|
.md-import-source { padding: 6px 12px; font-size: 11px; overflow-wrap: anywhere; color: var(--c-text-soft); border-top: 1px solid var(--c-line); }
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { canvasState } from '../../canvas-state.js';
|
|
3
|
-
import { addCanvasNode } from '../../canvas-operations.js';
|
|
3
|
+
import { addCanvasNode, createCanvasGroup } from '../../canvas-operations.js';
|
|
4
|
+
import { findOpenCanvasPosition, overlapsAny } from '../../../shared/placement.js';
|
|
5
|
+
import { GROUP_PAD, GROUP_TITLEBAR_HEIGHT } from '../../placement.js';
|
|
4
6
|
import {
|
|
5
7
|
MAX_IMPORT_AGENT_DESCRIPTION_LENGTH,
|
|
6
8
|
MAX_IMPORT_REASON_LENGTH,
|
|
@@ -192,6 +194,17 @@ const commit = defineOperation({
|
|
|
192
194
|
throw new OperationError('Open the captured source board before committing this import.', 409);
|
|
193
195
|
const a = canvasState.getAttachment(job.attachmentId);
|
|
194
196
|
if (!a) throw new OperationError('Attachment not found.', 404);
|
|
197
|
+
const grouped = job.sections.length > 1;
|
|
198
|
+
const padX = grouped ? GROUP_PAD : 0;
|
|
199
|
+
const padY = grouped ? GROUP_PAD + GROUP_TITLEBAR_HEIGHT : 0;
|
|
200
|
+
const width = Math.min(job.sections.length, 2) * 440 - 40 + padX * 2;
|
|
201
|
+
const height = Math.ceil(job.sections.length / 2) * 340 - 60 + padY + padX;
|
|
202
|
+
// Reserve the whole grid, including its group frame, against current board
|
|
203
|
+
// geometry. Another import or edit may have occupied the drop-time anchor.
|
|
204
|
+
const obstacles = canvasState.getLayout().nodes;
|
|
205
|
+
const origin = overlapsAny(job.position, width, height, obstacles, 48)
|
|
206
|
+
? findOpenCanvasPosition(obstacles, width, height, 48)
|
|
207
|
+
: job.position;
|
|
195
208
|
const nodeIds = job.sections.map(
|
|
196
209
|
(s, index) =>
|
|
197
210
|
addCanvasNode({
|
|
@@ -201,17 +214,20 @@ const commit = defineOperation({
|
|
|
201
214
|
data: {
|
|
202
215
|
source: { attachmentId: a.id, filename: a.name, ...(s.reference ? { reference: s.reference } : {}) },
|
|
203
216
|
},
|
|
204
|
-
x:
|
|
205
|
-
y:
|
|
217
|
+
x: origin.x + padX + (index % 2) * 440,
|
|
218
|
+
y: origin.y + padY + Math.floor(index / 2) * 340,
|
|
206
219
|
defaultWidth: 400,
|
|
207
220
|
defaultHeight: 280,
|
|
208
221
|
fileMode: 'auto',
|
|
209
222
|
}).id,
|
|
210
223
|
);
|
|
224
|
+
let groupId: string | undefined;
|
|
211
225
|
try {
|
|
226
|
+
if (grouped) groupId = createCanvasGroup({ title: a.name, childIds: nodeIds }).id;
|
|
212
227
|
const committed = canvasState.commitDocumentImport(i.id, nodeIds);
|
|
213
228
|
return { ok: true, nodeIds, import: committed };
|
|
214
229
|
} catch (error) {
|
|
230
|
+
if (groupId) canvasState.removeNode(groupId);
|
|
215
231
|
for (const nodeId of nodeIds) canvasState.removeNode(nodeId);
|
|
216
232
|
throw error;
|
|
217
233
|
}
|