@1agh/maude 0.15.0 → 0.17.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/README.md +4 -2
- package/cli/commands/design.mjs +108 -2
- package/package.json +12 -18
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +8 -8
- package/plugins/design/dev-server/annotations-layer.tsx +8 -10
- package/plugins/design/dev-server/api.ts +227 -3
- package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +40 -0
- package/plugins/design/dev-server/bin/_html-playwright.mjs +129 -0
- package/plugins/design/dev-server/bin/_pdf-playwright.mjs +105 -0
- package/plugins/design/dev-server/bin/_png-playwright.mjs +143 -0
- package/plugins/design/dev-server/bin/_pptx-playwright.mjs +98 -0
- package/plugins/design/dev-server/bin/_svg-playwright.mjs +141 -0
- package/plugins/design/dev-server/canvas-lib.tsx +12 -13
- package/plugins/design/dev-server/canvas-shell.tsx +111 -9
- package/plugins/design/dev-server/client/app.jsx +71 -143
- package/plugins/design/dev-server/client/comments-overlay.css +381 -0
- package/plugins/design/dev-server/client/styles/3-shell.css +1 -10
- package/plugins/design/dev-server/client/styles/4-components.css +5 -161
- package/plugins/design/dev-server/client/styles.css +5 -160
- package/plugins/design/dev-server/comments-overlay.tsx +1156 -0
- package/plugins/design/dev-server/context-menu.tsx +36 -9
- package/plugins/design/dev-server/dist/client.bundle.js +52 -211
- package/plugins/design/dev-server/dist/styles.css +1 -218
- package/plugins/design/dev-server/export-dialog.tsx +401 -0
- package/plugins/design/dev-server/exporters/_browser-bundles.ts +89 -0
- package/plugins/design/dev-server/exporters/canva-handoff-prompt.ts +74 -0
- package/plugins/design/dev-server/exporters/canva.ts +126 -0
- package/plugins/design/dev-server/exporters/html.ts +103 -0
- package/plugins/design/dev-server/exporters/index.ts +135 -0
- package/plugins/design/dev-server/exporters/pdf.ts +109 -0
- package/plugins/design/dev-server/exporters/png.ts +136 -0
- package/plugins/design/dev-server/exporters/pptx.ts +263 -0
- package/plugins/design/dev-server/exporters/scope.ts +196 -0
- package/plugins/design/dev-server/exporters/svg.ts +122 -0
- package/plugins/design/dev-server/exporters/zip.ts +109 -0
- package/plugins/design/dev-server/http.ts +109 -0
- package/plugins/design/dev-server/input-router.tsx +21 -0
- package/plugins/design/dev-server/inspect.ts +1 -1
- package/plugins/design/dev-server/server.mjs +1 -1
- package/plugins/design/dev-server/test/canvas-meta-api.test.ts +0 -10
- package/plugins/design/dev-server/test/comments-api.test.ts +229 -0
- package/plugins/design/dev-server/test/exporters/canva.test.ts +64 -0
- package/plugins/design/dev-server/test/exporters/endpoint.test.ts +121 -0
- package/plugins/design/dev-server/test/exporters/history.test.ts +79 -0
- package/plugins/design/dev-server/test/exporters/html.test.ts +26 -0
- package/plugins/design/dev-server/test/exporters/pdf.test.ts +53 -0
- package/plugins/design/dev-server/test/exporters/png.test.ts +32 -0
- package/plugins/design/dev-server/test/exporters/pptx.test.ts +31 -0
- package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
- package/plugins/design/dev-server/test/exporters/svg.test.ts +29 -0
- package/plugins/design/dev-server/test/exporters/zip.test.ts +105 -0
- package/plugins/design/dev-server/tool-palette.tsx +34 -16
- package/plugins/design/templates/_shell.html +33 -0
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
type ViewportControllerHandle,
|
|
43
43
|
useViewportControllerContext,
|
|
44
44
|
} from './canvas-lib.tsx';
|
|
45
|
+
import { CommentsOverlay } from './comments-overlay.tsx';
|
|
45
46
|
import {
|
|
46
47
|
ContextMenuProvider,
|
|
47
48
|
type ContextRegistry,
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
type MenuItem,
|
|
51
52
|
useContextMenu,
|
|
52
53
|
} from './context-menu.tsx';
|
|
54
|
+
import { ExportDialogProvider } from './export-dialog.tsx';
|
|
53
55
|
import { type HoverTarget, resolveHoverTarget, useInputRouter } from './input-router.tsx';
|
|
54
56
|
import { ToolPalette } from './tool-palette.tsx';
|
|
55
57
|
import {
|
|
@@ -202,9 +204,11 @@ function CanvasCore({
|
|
|
202
204
|
);
|
|
203
205
|
|
|
204
206
|
return (
|
|
205
|
-
<
|
|
206
|
-
<
|
|
207
|
-
|
|
207
|
+
<ExportDialogProvider>
|
|
208
|
+
<ContextMenuProvider registry={registry}>
|
|
209
|
+
<CanvasRouter hostRef={hostRef}>{children}</CanvasRouter>
|
|
210
|
+
</ContextMenuProvider>
|
|
211
|
+
</ExportDialogProvider>
|
|
208
212
|
);
|
|
209
213
|
}
|
|
210
214
|
|
|
@@ -262,6 +266,23 @@ function buildRegistry(deps: {
|
|
|
262
266
|
onSelect: () => controller?.reset(),
|
|
263
267
|
};
|
|
264
268
|
|
|
269
|
+
// Phase 6.5 — context-menu → ExportDialog. Each entry dispatches a custom
|
|
270
|
+
// event the dialog provider listens for; this avoids prop-drilling the
|
|
271
|
+
// dialog handle through every menu callback. The scope arg prefills the
|
|
272
|
+
// dialog's scope dropdown so the user lands on the right resolution.
|
|
273
|
+
const exportItem = (id: string, label: string, scope: string, shortcut?: string): MenuItem => ({
|
|
274
|
+
id,
|
|
275
|
+
label,
|
|
276
|
+
shortcut,
|
|
277
|
+
onSelect: () => {
|
|
278
|
+
try {
|
|
279
|
+
window.dispatchEvent(new CustomEvent('maude:open-export', { detail: { scope } }));
|
|
280
|
+
} catch {
|
|
281
|
+
/* non-window environments */
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
265
286
|
return {
|
|
266
287
|
element: [
|
|
267
288
|
[
|
|
@@ -297,6 +318,7 @@ function buildRegistry(deps: {
|
|
|
297
318
|
},
|
|
298
319
|
},
|
|
299
320
|
],
|
|
321
|
+
[exportItem('export-selection', 'Export selection…', 'selection', '⌘E')],
|
|
300
322
|
[
|
|
301
323
|
{
|
|
302
324
|
id: 'hide',
|
|
@@ -333,8 +355,15 @@ function buildRegistry(deps: {
|
|
|
333
355
|
fitItem,
|
|
334
356
|
resetItem,
|
|
335
357
|
],
|
|
358
|
+
[exportItem('export-artboard', 'Export this artboard…', 'artboard')],
|
|
359
|
+
],
|
|
360
|
+
world: [
|
|
361
|
+
[fitItem, resetItem],
|
|
362
|
+
[
|
|
363
|
+
exportItem('export-canvas', 'Export canvas as separate…', 'canvas-as-separate'),
|
|
364
|
+
exportItem('export-project', 'Export project (ZIP)…', 'project-raw'),
|
|
365
|
+
],
|
|
336
366
|
],
|
|
337
|
-
world: [[fitItem, resetItem]],
|
|
338
367
|
overlay: [],
|
|
339
368
|
};
|
|
340
369
|
}
|
|
@@ -469,17 +498,89 @@ function CanvasRouter({
|
|
|
469
498
|
setHoverEl(null);
|
|
470
499
|
},
|
|
471
500
|
onDropComment: ({ clientX, clientY }) => {
|
|
472
|
-
|
|
473
|
-
|
|
501
|
+
// First try deep mode — preferred when the user clicks exactly on
|
|
502
|
+
// a stamped element. When the deep hit lands on an element with
|
|
503
|
+
// `pointer-events: none` (decorative <svg> children, overlay icons),
|
|
504
|
+
// elementFromPoint propagates past it and `resolveHoverTarget`
|
|
505
|
+
// returns null because the next-closest hit is `.dc-artboard-body`
|
|
506
|
+
// itself.
|
|
507
|
+
let target = resolveHoverTarget(document, clientX, clientY, { deep: true });
|
|
508
|
+
if (!target) target = resolveHoverTarget(document, clientX, clientY, { deep: false });
|
|
509
|
+
// Phase 6 fallback — when both resolveHoverTarget passes bail (the
|
|
510
|
+
// `hit === bodyEl` early-exit triggers on `pointer-events: none`
|
|
511
|
+
// decorations), enumerate every element under the click point and
|
|
512
|
+
// climb the first one that has `data-cd-id`. This is how clicks on
|
|
513
|
+
// SVG logos / icon glyphs land on the actual stamped wrapper.
|
|
514
|
+
if (!target && typeof document.elementsFromPoint === 'function') {
|
|
515
|
+
const stack = document.elementsFromPoint(clientX, clientY);
|
|
516
|
+
for (const candidate of stack) {
|
|
517
|
+
const stamped = (candidate as Element).closest?.('[data-cd-id]') as HTMLElement | null;
|
|
518
|
+
if (!stamped) continue;
|
|
519
|
+
if (!stamped.closest('.dc-artboard-body')) continue;
|
|
520
|
+
const artboardEl = stamped.closest('[data-dc-screen]');
|
|
521
|
+
target = {
|
|
522
|
+
el: stamped,
|
|
523
|
+
cdId: stamped.getAttribute('data-cd-id'),
|
|
524
|
+
artboardId: artboardEl?.getAttribute('data-dc-screen') ?? null,
|
|
525
|
+
};
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (!target) {
|
|
530
|
+
// Floating comment fallback — no element anchor, just a click
|
|
531
|
+
// point. The overlay still renders a pin at the stored bounds.
|
|
532
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
533
|
+
const floatingSel: Selection = {
|
|
534
|
+
file: deriveFile(),
|
|
535
|
+
id: undefined,
|
|
536
|
+
selector: '',
|
|
537
|
+
artboardId: null,
|
|
538
|
+
tag: '',
|
|
539
|
+
classes: '',
|
|
540
|
+
text: '',
|
|
541
|
+
dom_path: [],
|
|
542
|
+
bounds: { x: clientX - 12, y: clientY - 12, w: 24, h: 24 },
|
|
543
|
+
html: '',
|
|
544
|
+
};
|
|
545
|
+
try {
|
|
546
|
+
document.dispatchEvent(
|
|
547
|
+
new CustomEvent('cm:open-composer', {
|
|
548
|
+
detail: { selection: floatingSel, clientX, clientY },
|
|
549
|
+
})
|
|
550
|
+
);
|
|
551
|
+
} catch {
|
|
552
|
+
/* ignore */
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
window.parent.postMessage({ dgn: 'comment-compose', selection: floatingSel }, '*');
|
|
556
|
+
} catch {
|
|
557
|
+
/* parent detached */
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
474
561
|
const sel = hoverTargetToSelection(target);
|
|
475
562
|
// Commit the target to the selection set so the halo persists while
|
|
476
|
-
// the
|
|
477
|
-
// - submit / cancel on the composer (
|
|
563
|
+
// the composer is open. The user clears by:
|
|
564
|
+
// - submit / cancel on the composer (overlay dispatches force-clear)
|
|
478
565
|
// - pressing Esc inside the canvas (router's onEscape → clear)
|
|
479
566
|
// - clicking another element in comment mode (this handler runs
|
|
480
567
|
// again and replaces)
|
|
481
568
|
selSet.replace(sel);
|
|
482
|
-
if (typeof window === 'undefined') return;
|
|
569
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
570
|
+
// Phase 6 — open the in-place composer inside the iframe at the click
|
|
571
|
+
// point. Custom event is iframe-local so the overlay can subscribe
|
|
572
|
+
// without round-tripping through the parent shell.
|
|
573
|
+
try {
|
|
574
|
+
document.dispatchEvent(
|
|
575
|
+
new CustomEvent('cm:open-composer', {
|
|
576
|
+
detail: { selection: sel, clientX, clientY },
|
|
577
|
+
})
|
|
578
|
+
);
|
|
579
|
+
} catch {
|
|
580
|
+
/* CustomEvent absent — fall through to legacy parent path */
|
|
581
|
+
}
|
|
582
|
+
// Still post to parent for back-compat with any legacy `.html` mocks
|
|
583
|
+
// whose inspector script consumes `comment-compose`.
|
|
483
584
|
try {
|
|
484
585
|
window.parent.postMessage({ dgn: 'comment-compose', selection: sel }, '*');
|
|
485
586
|
} catch {
|
|
@@ -492,6 +593,7 @@ function CanvasRouter({
|
|
|
492
593
|
return (
|
|
493
594
|
<>
|
|
494
595
|
{children}
|
|
596
|
+
<CommentsOverlay />
|
|
495
597
|
<AnnotationsLayer />
|
|
496
598
|
<ToolPalette />
|
|
497
599
|
<HoverHalo el={hoverEl} />
|
|
@@ -1208,64 +1208,21 @@ function Gallery({ title, items, onOpen, kind }) {
|
|
|
1208
1208
|
|
|
1209
1209
|
// ---------- Comment composer / viewer ----------
|
|
1210
1210
|
|
|
1211
|
-
function CommentBar({ activePath,
|
|
1211
|
+
function CommentBar({ activePath, comments }) {
|
|
1212
|
+
// Phase 6 — the shell-side composer + chip strip + focused-row chrome were
|
|
1213
|
+
// removed. The iframe overlay (comments-overlay.tsx) owns composer + pin
|
|
1214
|
+
// bubbles + thread popover. BottomBar shrinks to a live open-count summary
|
|
1215
|
+
// so the shell still surfaces total review activity at a glance.
|
|
1212
1216
|
if (!activePath) return null;
|
|
1213
|
-
const focused = focusedId ? comments.find(c => c.id === focusedId) : null;
|
|
1214
1217
|
const openComments = (comments || []).filter(c => c.status !== 'resolved');
|
|
1218
|
+
if (openComments.length === 0) return null;
|
|
1215
1219
|
return (
|
|
1216
1220
|
<div className="comment-bar">
|
|
1217
|
-
|
|
1218
|
-
<
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
</div>
|
|
1223
|
-
<textarea
|
|
1224
|
-
autoFocus
|
|
1225
|
-
className="composer-textarea"
|
|
1226
|
-
value={draft.text}
|
|
1227
|
-
placeholder="What should change here? (⌘↵ save · Esc cancel)"
|
|
1228
|
-
onChange={e => setDraft({ ...draft, text: e.target.value })}
|
|
1229
|
-
onKeyDown={e => {
|
|
1230
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); onSubmit(); }
|
|
1231
|
-
else if (e.key === 'Escape') { e.preventDefault(); onCancel(); }
|
|
1232
|
-
}}
|
|
1233
|
-
rows={4}
|
|
1234
|
-
/>
|
|
1235
|
-
<div className="composer-actions">
|
|
1236
|
-
<button className="cb-secondary" onClick={onCancel}>Cancel</button>
|
|
1237
|
-
<button className="cb-primary" disabled={!draft.text.trim()} onClick={onSubmit}>Save · ⌘↵</button>
|
|
1238
|
-
</div>
|
|
1239
|
-
</div>
|
|
1240
|
-
)}
|
|
1241
|
-
|
|
1242
|
-
{focused && (
|
|
1243
|
-
<div className="cb-row focused">
|
|
1244
|
-
<span className="cb-pinno">#{(comments || []).filter(c => c.selector).findIndex(c => c.id === focused.id) + 1}</span>
|
|
1245
|
-
<span className="cb-text">{focused.text}</span>
|
|
1246
|
-
<span className="cb-target" title={focused.dom_path ? focused.dom_path.join(' > ') : ''}>
|
|
1247
|
-
<code>{focused.selector || '—'}</code>
|
|
1248
|
-
</span>
|
|
1249
|
-
{focused.status === 'resolved'
|
|
1250
|
-
? <button className="cb-secondary" onClick={() => onReopen(focused.id)}>Reopen</button>
|
|
1251
|
-
: <button className="cb-primary" onClick={() => onResolve(focused.id)}>✓ Resolve</button>}
|
|
1252
|
-
<button className="cb-secondary" onClick={() => onDelete(focused.id)}>Delete</button>
|
|
1253
|
-
</div>
|
|
1254
|
-
)}
|
|
1255
|
-
|
|
1256
|
-
{!draft && !focused && openComments.length > 0 && (
|
|
1257
|
-
<div className="cb-row strip">
|
|
1258
|
-
<span className="cb-label">{openComments.length} open comment{openComments.length === 1 ? '' : 's'}</span>
|
|
1259
|
-
<div className="cb-pin-strip">
|
|
1260
|
-
{openComments.slice(0, 12).map((c, i) => (
|
|
1261
|
-
<button key={c.id} className="cb-pin-chip" title={c.text} onClick={() => onFocusPin(c.id)}>
|
|
1262
|
-
{i + 1}
|
|
1263
|
-
</button>
|
|
1264
|
-
))}
|
|
1265
|
-
{openComments.length > 12 && <span className="cb-more">+{openComments.length - 12}</span>}
|
|
1266
|
-
</div>
|
|
1267
|
-
</div>
|
|
1268
|
-
)}
|
|
1221
|
+
<div className="cb-row strip">
|
|
1222
|
+
<span className="cb-label">
|
|
1223
|
+
{openComments.length} open comment{openComments.length === 1 ? '' : 's'}
|
|
1224
|
+
</span>
|
|
1225
|
+
</div>
|
|
1269
1226
|
</div>
|
|
1270
1227
|
);
|
|
1271
1228
|
}
|
|
@@ -1278,7 +1235,7 @@ function StatusBarSlot({ label, children, className = '' }) {
|
|
|
1278
1235
|
);
|
|
1279
1236
|
}
|
|
1280
1237
|
|
|
1281
|
-
function StatusBar({ activePath, selected, wsConnected, openCount, theme, onToggleTheme, onClearSelected
|
|
1238
|
+
function StatusBar({ activePath, selected, wsConnected, openCount, theme, onToggleTheme, onClearSelected }) {
|
|
1282
1239
|
const isSystem = activePath === SYSTEM_TAB;
|
|
1283
1240
|
const text = selected && selected.selector
|
|
1284
1241
|
? selected.selector + (selected.text ? ` — "${selected.text.slice(0, 60)}"` : '')
|
|
@@ -1297,9 +1254,6 @@ function StatusBar({ activePath, selected, wsConnected, openCount, theme, onTogg
|
|
|
1297
1254
|
<StatusBarSlot label="Selected element" className="sb-selected">
|
|
1298
1255
|
<span className="sb-dot" aria-hidden="true">●</span>
|
|
1299
1256
|
<span className="sb-sel-text" title={title}>{text}</span>
|
|
1300
|
-
{!hasDraft && (
|
|
1301
|
-
<button type="button" className="sb-add-comment" onClick={onAddComment} title="Add comment on selected element (⌘⇧+click in canvas)">+ comment</button>
|
|
1302
|
-
)}
|
|
1303
1257
|
<button type="button" className="sb-clear-sel" onClick={onClearSelected} title="Clear (Esc inside iframe)" aria-label="Clear selection">×</button>
|
|
1304
1258
|
</StatusBarSlot>
|
|
1305
1259
|
)}
|
|
@@ -1454,7 +1408,8 @@ function App() {
|
|
|
1454
1408
|
return () => { cancelled = true; };
|
|
1455
1409
|
}, []);
|
|
1456
1410
|
const [commentsByFile, setCommentsByFile] = useState({}); // { file: [Comment] }
|
|
1457
|
-
|
|
1411
|
+
// Phase 6 — the in-iframe composer owns drafting; the shell no longer holds
|
|
1412
|
+
// a `draft` state. Mutations route through postMessage → WS instead.
|
|
1458
1413
|
const [focusedCommentId, setFocusedCommentId] = useState(null);
|
|
1459
1414
|
const [commentsPanelOpen, setCommentsPanelOpen] = useState(false);
|
|
1460
1415
|
const [commentsFilter, setCommentsFilter] = useState('open'); // 'all' | 'open' | 'resolved'
|
|
@@ -1609,7 +1564,6 @@ function App() {
|
|
|
1609
1564
|
});
|
|
1610
1565
|
setActivePath(path);
|
|
1611
1566
|
setFocusedCommentId(null);
|
|
1612
|
-
setDraft(null);
|
|
1613
1567
|
}, []);
|
|
1614
1568
|
|
|
1615
1569
|
const openSystem = useCallback(() => {
|
|
@@ -1672,30 +1626,6 @@ function App() {
|
|
|
1672
1626
|
try { el.contentWindow.postMessage({ dgn: 'comments-set', comments: list }, '*'); } catch {}
|
|
1673
1627
|
}, [activePath, commentsByFile]);
|
|
1674
1628
|
|
|
1675
|
-
// ----- Comment composer helpers -----
|
|
1676
|
-
// Declared BEFORE the inbound-message useEffect that references them — under
|
|
1677
|
-
// ES build (no var-style hoisting) these are real TDZ violations otherwise.
|
|
1678
|
-
const startDraftFor = useCallback((sel) => {
|
|
1679
|
-
const file = (sel && sel.file) || activePath;
|
|
1680
|
-
if (!file || file === SYSTEM_TAB) return;
|
|
1681
|
-
setDraft({
|
|
1682
|
-
file,
|
|
1683
|
-
selector: sel?.selector || '',
|
|
1684
|
-
dom_path: sel?.dom_path || [],
|
|
1685
|
-
tag: sel?.tag || '',
|
|
1686
|
-
classes: sel?.classes || '',
|
|
1687
|
-
bounds: sel?.bounds || null,
|
|
1688
|
-
html: sel?.html || '',
|
|
1689
|
-
text: '',
|
|
1690
|
-
});
|
|
1691
|
-
setFocusedCommentId(null);
|
|
1692
|
-
}, [activePath]);
|
|
1693
|
-
|
|
1694
|
-
const startDraftFromSelection = useCallback(() => {
|
|
1695
|
-
if (!selected || !selected.selector) return;
|
|
1696
|
-
startDraftFor(selected);
|
|
1697
|
-
}, [selected, startDraftFor]);
|
|
1698
|
-
|
|
1699
1629
|
// ----- Inbound messages from iframes -----
|
|
1700
1630
|
useEffect(() => {
|
|
1701
1631
|
function onMessage(e) {
|
|
@@ -1728,13 +1658,40 @@ function App() {
|
|
|
1728
1658
|
wsSend({ type: 'clear-select' });
|
|
1729
1659
|
setSelected(null);
|
|
1730
1660
|
} else if (m.dgn === 'comment-compose' && m.selection) {
|
|
1731
|
-
//
|
|
1732
|
-
//
|
|
1733
|
-
startDraftFor
|
|
1734
|
-
|
|
1735
|
-
//
|
|
1736
|
-
//
|
|
1737
|
-
|
|
1661
|
+
// Phase 6 — the iframe overlay owns the composer surface now. The
|
|
1662
|
+
// shell just mirrors `selected` so the StatusBar / sidebar still
|
|
1663
|
+
// reflect the target, and skips the legacy `startDraftFor` path that
|
|
1664
|
+
// opened the shell-side composer. Legacy `.html` mocks (no
|
|
1665
|
+
// canvas-shell mount) fall through to the same path; they lose the
|
|
1666
|
+
// shell composer in this phase. Acceptable per Phase 6 scope.
|
|
1667
|
+
setSelected(m.selection);
|
|
1668
|
+
} else if (m.dgn === 'comment-submit' && m.payload && typeof m.payload.text === 'string') {
|
|
1669
|
+
// Phase 6 — iframe overlay finished composing. Relay through the
|
|
1670
|
+
// existing WS `comments-add` channel; server-side persistence +
|
|
1671
|
+
// broadcast back are identical to the legacy shell-composer flow.
|
|
1672
|
+
const p = m.payload;
|
|
1673
|
+
const txt = String(p.text).trim();
|
|
1674
|
+
if (txt) {
|
|
1675
|
+
wsSend({
|
|
1676
|
+
type: 'comments-add',
|
|
1677
|
+
payload: {
|
|
1678
|
+
file: p.file,
|
|
1679
|
+
selector: p.selector,
|
|
1680
|
+
dom_path: p.dom_path,
|
|
1681
|
+
tag: p.tag,
|
|
1682
|
+
classes: p.classes,
|
|
1683
|
+
bounds: p.bounds,
|
|
1684
|
+
html_excerpt: p.html_excerpt,
|
|
1685
|
+
text: txt,
|
|
1686
|
+
},
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
} else if (m.dgn === 'comment-patch' && m.id && m.patch && typeof m.patch === 'object') {
|
|
1690
|
+
// Phase 6 — thread popover routes resolve / reopen through here.
|
|
1691
|
+
wsSend({ type: 'comments-patch', id: m.id, patch: m.patch });
|
|
1692
|
+
} else if (m.dgn === 'comment-delete' && m.id) {
|
|
1693
|
+
wsSend({ type: 'comments-delete', id: m.id });
|
|
1694
|
+
setFocusedCommentId(prev => (prev === m.id ? null : prev));
|
|
1738
1695
|
} else if (m.dgn === 'comment-click' && m.id) {
|
|
1739
1696
|
setFocusedCommentId(m.id);
|
|
1740
1697
|
} else if (m.dgn === 'loaded' && m.file) {
|
|
@@ -1751,7 +1708,7 @@ function App() {
|
|
|
1751
1708
|
}
|
|
1752
1709
|
window.addEventListener('message', onMessage);
|
|
1753
1710
|
return () => window.removeEventListener('message', onMessage);
|
|
1754
|
-
}, [commentsByFile, focusedCommentId
|
|
1711
|
+
}, [commentsByFile, focusedCommentId]);
|
|
1755
1712
|
|
|
1756
1713
|
// Tell the active canvas iframe to drop any persistent selection (canvas
|
|
1757
1714
|
// SelectionSet) — used when the comment composer closes via submit /
|
|
@@ -1765,27 +1722,6 @@ function App() {
|
|
|
1765
1722
|
}
|
|
1766
1723
|
}, [activePath]);
|
|
1767
1724
|
|
|
1768
|
-
const submitDraft = useCallback(() => {
|
|
1769
|
-
if (!draft || !draft.text.trim()) return;
|
|
1770
|
-
wsSend({ type: 'comments-add', payload: {
|
|
1771
|
-
file: draft.file,
|
|
1772
|
-
selector: draft.selector,
|
|
1773
|
-
dom_path: draft.dom_path,
|
|
1774
|
-
tag: draft.tag,
|
|
1775
|
-
classes: draft.classes,
|
|
1776
|
-
bounds: draft.bounds,
|
|
1777
|
-
html_excerpt: draft.html,
|
|
1778
|
-
text: draft.text.trim(),
|
|
1779
|
-
}});
|
|
1780
|
-
setDraft(null);
|
|
1781
|
-
clearActiveCanvasSelection();
|
|
1782
|
-
}, [draft, clearActiveCanvasSelection]);
|
|
1783
|
-
|
|
1784
|
-
const cancelDraft = useCallback(() => {
|
|
1785
|
-
setDraft(null);
|
|
1786
|
-
clearActiveCanvasSelection();
|
|
1787
|
-
}, [clearActiveCanvasSelection]);
|
|
1788
|
-
|
|
1789
1725
|
const resolveComment = useCallback((id) => {
|
|
1790
1726
|
wsSend({ type: 'comments-patch', id, patch: { status: 'resolved' } });
|
|
1791
1727
|
}, []);
|
|
@@ -1797,16 +1733,6 @@ function App() {
|
|
|
1797
1733
|
setFocusedCommentId(prev => (prev === id ? null : prev));
|
|
1798
1734
|
}, []);
|
|
1799
1735
|
|
|
1800
|
-
const focusPinFromBar = useCallback((id) => {
|
|
1801
|
-
setFocusedCommentId(id);
|
|
1802
|
-
if (activePath && activePath !== SYSTEM_TAB) {
|
|
1803
|
-
const el = iframesRef.current.get(activePath);
|
|
1804
|
-
if (el && el.contentWindow) {
|
|
1805
|
-
try { el.contentWindow.postMessage({ dgn: 'comment-focus', id }, '*'); } catch {}
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
}, [activePath]);
|
|
1809
|
-
|
|
1810
1736
|
// Jump from right-sidebar list to a comment: open file tab if needed, focus pin.
|
|
1811
1737
|
// The iframe may be freshly mounted; the loaded handler also re-sends focus if focusedCommentId matches.
|
|
1812
1738
|
const jumpToComment = useCallback((file, id) => {
|
|
@@ -1918,15 +1844,15 @@ function App() {
|
|
|
1918
1844
|
setHelpOpen(true);
|
|
1919
1845
|
return;
|
|
1920
1846
|
}
|
|
1921
|
-
// Esc —
|
|
1847
|
+
// Esc — clear focused pin. The in-place composer (Phase 6) and thread
|
|
1848
|
+
// popover handle their own Esc inside the iframe.
|
|
1922
1849
|
if (e.key === 'Escape') {
|
|
1923
|
-
if (draft) { setDraft(null); clearActiveCanvasSelection(); return; }
|
|
1924
1850
|
if (focusedCommentId) { setFocusedCommentId(null); return; }
|
|
1925
1851
|
}
|
|
1926
1852
|
}
|
|
1927
1853
|
window.addEventListener('keydown', onKey);
|
|
1928
1854
|
return () => window.removeEventListener('keydown', onKey);
|
|
1929
|
-
}, [reloadActive, selected, activePath,
|
|
1855
|
+
}, [reloadActive, selected, activePath, focusedCommentId, sidebarOpen, openSystem, closeTab, clearActiveCanvasSelection]);
|
|
1930
1856
|
|
|
1931
1857
|
const registerIframe = useCallback((path, el) => {
|
|
1932
1858
|
if (el) iframesRef.current.set(path, el);
|
|
@@ -1935,8 +1861,25 @@ function App() {
|
|
|
1935
1861
|
const activeFileComments = (activePath && activePath !== SYSTEM_TAB) ? (commentsByFile[activePath] || []) : [];
|
|
1936
1862
|
const totalOpen = totalCounts(commentsByFile).open;
|
|
1937
1863
|
|
|
1864
|
+
// Suppress the native browser context menu across the shell — the canvas
|
|
1865
|
+
// input-router already handles right-click inside the canvas host, but
|
|
1866
|
+
// sidebar / menubar / statusbar / floating chrome would otherwise leak the
|
|
1867
|
+
// native menu on top of our `.dc-context-menu` (or alone, outside canvas).
|
|
1868
|
+
// Editable fields (search box, future text inputs) keep the native menu so
|
|
1869
|
+
// copy/paste still works.
|
|
1870
|
+
const onShellContextMenu = useCallback((e) => {
|
|
1871
|
+
const t = e.target;
|
|
1872
|
+
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || (t.isContentEditable))) {
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
e.preventDefault();
|
|
1876
|
+
}, []);
|
|
1877
|
+
|
|
1938
1878
|
return (
|
|
1939
|
-
<div
|
|
1879
|
+
<div
|
|
1880
|
+
className={'app' + (commentsPanelOpen ? ' with-rsidebar' : '') + (sidebarOpen ? '' : ' no-sidebar')}
|
|
1881
|
+
onContextMenu={onShellContextMenu}
|
|
1882
|
+
>
|
|
1940
1883
|
<Sidebar
|
|
1941
1884
|
groups={groups}
|
|
1942
1885
|
activePath={activePath}
|
|
@@ -1979,20 +1922,7 @@ function App() {
|
|
|
1979
1922
|
cfg={cfg}
|
|
1980
1923
|
/>
|
|
1981
1924
|
{activePath && activePath !== SYSTEM_TAB && (
|
|
1982
|
-
<CommentBar
|
|
1983
|
-
activePath={activePath}
|
|
1984
|
-
selected={selected}
|
|
1985
|
-
comments={activeFileComments}
|
|
1986
|
-
focusedId={focusedCommentId}
|
|
1987
|
-
draft={draft && draft.file === activePath ? draft : null}
|
|
1988
|
-
setDraft={setDraft}
|
|
1989
|
-
onSubmit={submitDraft}
|
|
1990
|
-
onCancel={cancelDraft}
|
|
1991
|
-
onResolve={resolveComment}
|
|
1992
|
-
onReopen={reopenComment}
|
|
1993
|
-
onDelete={deleteComment}
|
|
1994
|
-
onFocusPin={focusPinFromBar}
|
|
1995
|
-
/>
|
|
1925
|
+
<CommentBar activePath={activePath} comments={activeFileComments} />
|
|
1996
1926
|
)}
|
|
1997
1927
|
<StatusBar
|
|
1998
1928
|
activePath={activePath}
|
|
@@ -2002,8 +1932,6 @@ function App() {
|
|
|
2002
1932
|
theme={theme}
|
|
2003
1933
|
onToggleTheme={toggleTheme}
|
|
2004
1934
|
onClearSelected={clearSelected}
|
|
2005
|
-
onAddComment={startDraftFromSelection}
|
|
2006
|
-
hasDraft={!!(draft && draft.file === activePath)}
|
|
2007
1935
|
/>
|
|
2008
1936
|
</div>
|
|
2009
1937
|
{commentsPanelOpen && (
|