@1agh/maude 0.15.0 → 0.16.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/README.md +1 -3
- package/package.json +8 -8
- package/plugins/design/dev-server/api.ts +186 -3
- package/plugins/design/dev-server/canvas-shell.tsx +79 -5
- package/plugins/design/dev-server/client/app.jsx +53 -142
- 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/dist/client.bundle.js +44 -211
- package/plugins/design/dev-server/dist/styles.css +1 -218
- package/plugins/design/dev-server/http.ts +29 -0
- package/plugins/design/dev-server/input-router.tsx +21 -0
- 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/README.md
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
# Maude
|
|
1
|
+
# Maude
|
|
2
2
|
|
|
3
3
|
A personal marketplace of Claude Code plugins. Two plugins today, plus a `maude` CLI for scaffolding and running the bundled dev tooling.
|
|
4
4
|
|
|
5
|
-
> **Renamed from `md-claude`.** See [`docs/MIGRATING-MD-CLAUDE-TO-MAUDE.md`](./docs/MIGRATING-MD-CLAUDE-TO-MAUDE.md) if you came from the old name.
|
|
6
|
-
|
|
7
5
|
> **📚 Full docs: https://maude.iagh.cz** (or browse the source under [`site/content/docs/`](./site/content/docs/) until the public URL lands).
|
|
8
6
|
> Contributing? See [CONTRIBUTING.md](./CONTRIBUTING.md). Security? See [SECURITY.md](./SECURITY.md).
|
|
9
7
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agh/maude",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
"prepublishOnly": "bash scripts/check-version-parity.sh"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@1agh/maude-darwin-arm64": "0.
|
|
43
|
-
"@1agh/maude-darwin-x64": "0.
|
|
44
|
-
"@1agh/maude-linux-arm64": "0.
|
|
45
|
-
"@1agh/maude-linux-arm64-musl": "0.
|
|
46
|
-
"@1agh/maude-linux-x64": "0.
|
|
47
|
-
"@1agh/maude-linux-x64-musl": "0.
|
|
48
|
-
"@1agh/maude-win32-x64": "0.
|
|
42
|
+
"@1agh/maude-darwin-arm64": "0.16.0",
|
|
43
|
+
"@1agh/maude-darwin-x64": "0.16.0",
|
|
44
|
+
"@1agh/maude-linux-arm64": "0.16.0",
|
|
45
|
+
"@1agh/maude-linux-arm64-musl": "0.16.0",
|
|
46
|
+
"@1agh/maude-linux-x64": "0.16.0",
|
|
47
|
+
"@1agh/maude-linux-x64-musl": "0.16.0",
|
|
48
|
+
"@1agh/maude-win32-x64": "0.16.0"
|
|
49
49
|
},
|
|
50
50
|
"files": [
|
|
51
51
|
"cli",
|
|
@@ -78,6 +78,19 @@ async function findFiles(absRoot: string, prefix: string, exts: string[]): Promi
|
|
|
78
78
|
|
|
79
79
|
// ---------- Comments ----------
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Phase 6 — single reply on a comment thread. `id` is `r_<hex>`; persists inside
|
|
83
|
+
* the parent `Comment.thread[]`. Bodies are bounded the same way as comment
|
|
84
|
+
* bodies (4000 chars), and `@handle` tokens in `body` flow into the parent's
|
|
85
|
+
* `mentions[]` union.
|
|
86
|
+
*/
|
|
87
|
+
export interface Reply {
|
|
88
|
+
id: string;
|
|
89
|
+
author: string;
|
|
90
|
+
body: string;
|
|
91
|
+
created: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
export interface Comment {
|
|
82
95
|
id: string;
|
|
83
96
|
file: string;
|
|
@@ -91,6 +104,19 @@ export interface Comment {
|
|
|
91
104
|
status: 'open' | 'resolved';
|
|
92
105
|
created: string;
|
|
93
106
|
resolved_at: string | null;
|
|
107
|
+
// Phase 6 — author + threading + mentions. Default-filled on read for legacy
|
|
108
|
+
// comments missing these fields (see `loadCommentsForFile`); persisted on next
|
|
109
|
+
// write. `author` defaults to the local `git config user.name` resolved at
|
|
110
|
+
// create time, `thread` to `[]`, `mentions` to `[]`.
|
|
111
|
+
author: string;
|
|
112
|
+
thread: Reply[];
|
|
113
|
+
mentions: string[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface GitCommitter {
|
|
117
|
+
name: string;
|
|
118
|
+
email: string;
|
|
119
|
+
commits: number;
|
|
94
120
|
}
|
|
95
121
|
|
|
96
122
|
export interface Api {
|
|
@@ -102,6 +128,9 @@ export interface Api {
|
|
|
102
128
|
commentsAdd(payload: Partial<Comment> & { file: string; text: string }): Promise<Comment | null>;
|
|
103
129
|
commentsPatch(id: string, patch: Partial<Comment>): Promise<Comment | null>;
|
|
104
130
|
commentsDelete(id: string): Promise<boolean>;
|
|
131
|
+
commentsAddReply(id: string, payload: { body: string; author?: string }): Promise<Comment | null>;
|
|
132
|
+
gitCommitters(): Promise<GitCommitter[]>;
|
|
133
|
+
parseMentions(text: string): string[];
|
|
105
134
|
// Canvas state
|
|
106
135
|
loadCanvasState(file: string): Promise<Record<string, unknown> | null>;
|
|
107
136
|
saveCanvasState(file: string, state: Record<string, unknown>): Promise<void>;
|
|
@@ -147,12 +176,26 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
|
|
|
147
176
|
try {
|
|
148
177
|
const raw = await Bun.file(commentsPath(file)).text();
|
|
149
178
|
const arr = JSON.parse(raw);
|
|
150
|
-
|
|
179
|
+
if (!Array.isArray(arr)) return [];
|
|
180
|
+
// Phase 6 — default-fill `author` / `thread` / `mentions` for legacy
|
|
181
|
+
// rows. No write-back here; the on-disk shape stays stable until the
|
|
182
|
+
// next mutation persists the upgraded record.
|
|
183
|
+
return arr.map(backfillComment);
|
|
151
184
|
} catch {
|
|
152
185
|
return [];
|
|
153
186
|
}
|
|
154
187
|
}
|
|
155
188
|
|
|
189
|
+
function backfillComment(raw: unknown): Comment {
|
|
190
|
+
const c = (raw ?? {}) as Partial<Comment>;
|
|
191
|
+
return {
|
|
192
|
+
...(c as Comment),
|
|
193
|
+
author: typeof c.author === 'string' ? c.author : '',
|
|
194
|
+
thread: Array.isArray(c.thread) ? c.thread : [],
|
|
195
|
+
mentions: Array.isArray(c.mentions) ? c.mentions : [],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
156
199
|
async function saveCommentsForFile(file: string, list: Comment[]) {
|
|
157
200
|
await Bun.write(commentsPath(file), JSON.stringify(list, null, 2));
|
|
158
201
|
}
|
|
@@ -172,7 +215,8 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
|
|
|
172
215
|
const arr = JSON.parse(raw);
|
|
173
216
|
if (!Array.isArray(arr) || arr.length === 0) continue;
|
|
174
217
|
const file = arr[0]?.file as string | undefined;
|
|
175
|
-
|
|
218
|
+
// Backfill legacy rows so callers see the v2 shape uniformly.
|
|
219
|
+
if (file) out[file] = arr.map(backfillComment);
|
|
176
220
|
} catch {
|
|
177
221
|
/* ignore */
|
|
178
222
|
}
|
|
@@ -184,10 +228,111 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
|
|
|
184
228
|
return `c_${crypto.randomBytes(6).toString('hex')}`;
|
|
185
229
|
}
|
|
186
230
|
|
|
231
|
+
function newReplyId(): string {
|
|
232
|
+
return `r_${crypto.randomBytes(6).toString('hex')}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------- Git author resolution ----------
|
|
236
|
+
//
|
|
237
|
+
// Author defaults flow from `git config user.name` resolved against the
|
|
238
|
+
// repo root. Cached for the lifetime of the process — the local git
|
|
239
|
+
// identity doesn't shift mid-session and `Bun.spawn` is cheap-but-not-free.
|
|
240
|
+
|
|
241
|
+
let cachedGitUser: string | null = null;
|
|
242
|
+
let cachedGitUserAttempted = false;
|
|
243
|
+
async function gitCurrentUser(): Promise<string> {
|
|
244
|
+
if (cachedGitUserAttempted) return cachedGitUser ?? '';
|
|
245
|
+
cachedGitUserAttempted = true;
|
|
246
|
+
try {
|
|
247
|
+
const proc = Bun.spawn(['git', 'config', 'user.name'], {
|
|
248
|
+
cwd: paths.repoRoot,
|
|
249
|
+
stdout: 'pipe',
|
|
250
|
+
stderr: 'pipe',
|
|
251
|
+
});
|
|
252
|
+
const out = await new Response(proc.stdout).text();
|
|
253
|
+
await proc.exited;
|
|
254
|
+
const name = out.trim();
|
|
255
|
+
cachedGitUser = name || null;
|
|
256
|
+
} catch {
|
|
257
|
+
cachedGitUser = null;
|
|
258
|
+
}
|
|
259
|
+
return cachedGitUser ?? '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// `git shortlog -sne` against the repo head — cached for 60 s so the
|
|
263
|
+
// @mention popup doesn't re-fork git on every keystroke.
|
|
264
|
+
let cachedCommitters: GitCommitter[] | null = null;
|
|
265
|
+
let cachedCommittersAt = 0;
|
|
266
|
+
async function gitCommitters(): Promise<GitCommitter[]> {
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
if (cachedCommitters && now - cachedCommittersAt < 60_000) return cachedCommitters;
|
|
269
|
+
try {
|
|
270
|
+
const proc = Bun.spawn(['git', 'shortlog', '-sne', 'HEAD'], {
|
|
271
|
+
cwd: paths.repoRoot,
|
|
272
|
+
stdout: 'pipe',
|
|
273
|
+
stderr: 'pipe',
|
|
274
|
+
});
|
|
275
|
+
const text = await new Response(proc.stdout).text();
|
|
276
|
+
await proc.exited;
|
|
277
|
+
const lines = text
|
|
278
|
+
.split('\n')
|
|
279
|
+
.map((l) => l.trim())
|
|
280
|
+
.filter(Boolean)
|
|
281
|
+
.slice(0, 20);
|
|
282
|
+
const out: GitCommitter[] = [];
|
|
283
|
+
for (const line of lines) {
|
|
284
|
+
// Format: `<spaces><count>\t<name> <<email>>`
|
|
285
|
+
const m = line.match(/^(\d+)\s+(.+?)\s+<([^>]+)>$/);
|
|
286
|
+
if (!m) continue;
|
|
287
|
+
const commits = Number(m[1]);
|
|
288
|
+
const name = m[2]?.trim() ?? '';
|
|
289
|
+
const email = m[3]?.trim() ?? '';
|
|
290
|
+
if (!name) continue;
|
|
291
|
+
out.push({ name, email, commits });
|
|
292
|
+
}
|
|
293
|
+
cachedCommitters = out;
|
|
294
|
+
cachedCommittersAt = now;
|
|
295
|
+
return out;
|
|
296
|
+
} catch {
|
|
297
|
+
cachedCommitters = cachedCommitters ?? [];
|
|
298
|
+
cachedCommittersAt = now;
|
|
299
|
+
return cachedCommitters;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Extract `@handle` tokens from free text. Deduped, returns the literal
|
|
305
|
+
* `@name` form (matching what the autocomplete inserts), so a comment with
|
|
306
|
+
* `"@ada @lin @ada"` collapses to `["@ada","@lin"]`.
|
|
307
|
+
*/
|
|
308
|
+
function parseMentions(text: string): string[] {
|
|
309
|
+
const out: string[] = [];
|
|
310
|
+
const seen = new Set<string>();
|
|
311
|
+
if (typeof text !== 'string' || !text) return out;
|
|
312
|
+
const re = /@[\w][\w.-]*/g;
|
|
313
|
+
for (const m of text.matchAll(re)) {
|
|
314
|
+
const tok = m[0];
|
|
315
|
+
if (!tok || seen.has(tok)) continue;
|
|
316
|
+
seen.add(tok);
|
|
317
|
+
out.push(tok);
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function mentionsUnion(c: Comment): string[] {
|
|
323
|
+
const all = [c.text, ...c.thread.map((r) => r.body)].join('\n');
|
|
324
|
+
return parseMentions(all);
|
|
325
|
+
}
|
|
326
|
+
|
|
187
327
|
async function commentsAdd(payload: Partial<Comment> & { file: string; text: string }) {
|
|
188
328
|
if (!payload || typeof payload.file !== 'string' || !payload.file) return null;
|
|
189
329
|
if (typeof payload.text !== 'string' || !payload.text.trim()) return null;
|
|
190
330
|
const list = await loadCommentsForFile(payload.file);
|
|
331
|
+
const text = String(payload.text).trim().slice(0, 4000);
|
|
332
|
+
const author =
|
|
333
|
+
typeof payload.author === 'string' && payload.author.trim()
|
|
334
|
+
? payload.author.trim().slice(0, 120)
|
|
335
|
+
: await gitCurrentUser();
|
|
191
336
|
const c: Comment = {
|
|
192
337
|
id: newCommentId(),
|
|
193
338
|
file: payload.file,
|
|
@@ -197,10 +342,13 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
|
|
|
197
342
|
classes: String(payload.classes || ''),
|
|
198
343
|
bounds: payload.bounds ?? null,
|
|
199
344
|
html_excerpt: String(payload.html_excerpt || '').slice(0, 2000),
|
|
200
|
-
text
|
|
345
|
+
text,
|
|
201
346
|
status: 'open',
|
|
202
347
|
created: new Date().toISOString(),
|
|
203
348
|
resolved_at: null,
|
|
349
|
+
author,
|
|
350
|
+
thread: [],
|
|
351
|
+
mentions: parseMentions(text),
|
|
204
352
|
};
|
|
205
353
|
list.push(c);
|
|
206
354
|
await saveCommentsForFile(payload.file, list);
|
|
@@ -208,6 +356,37 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
|
|
|
208
356
|
return c;
|
|
209
357
|
}
|
|
210
358
|
|
|
359
|
+
async function commentsAddReply(
|
|
360
|
+
id: string,
|
|
361
|
+
payload: { body: string; author?: string }
|
|
362
|
+
): Promise<Comment | null> {
|
|
363
|
+
if (!payload || typeof payload.body !== 'string' || !payload.body.trim()) return null;
|
|
364
|
+
const all = await loadAllComments();
|
|
365
|
+
for (const [file, list] of Object.entries(all)) {
|
|
366
|
+
const i = list.findIndex((c) => c.id === id);
|
|
367
|
+
if (i < 0) continue;
|
|
368
|
+
const entry = list[i];
|
|
369
|
+
if (!entry) continue;
|
|
370
|
+
const body = payload.body.trim().slice(0, 4000);
|
|
371
|
+
const author =
|
|
372
|
+
typeof payload.author === 'string' && payload.author.trim()
|
|
373
|
+
? payload.author.trim().slice(0, 120)
|
|
374
|
+
: await gitCurrentUser();
|
|
375
|
+
const reply: Reply = {
|
|
376
|
+
id: newReplyId(),
|
|
377
|
+
author,
|
|
378
|
+
body,
|
|
379
|
+
created: new Date().toISOString(),
|
|
380
|
+
};
|
|
381
|
+
entry.thread = [...entry.thread, reply];
|
|
382
|
+
entry.mentions = mentionsUnion(entry);
|
|
383
|
+
await saveCommentsForFile(file, list);
|
|
384
|
+
onCommentsChanged(file);
|
|
385
|
+
return entry;
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
211
390
|
async function commentsPatch(id: string, patch: Partial<Comment>) {
|
|
212
391
|
const all = await loadAllComments();
|
|
213
392
|
for (const [file, list] of Object.entries(all)) {
|
|
@@ -221,6 +400,7 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
|
|
|
221
400
|
}
|
|
222
401
|
if (typeof patch.text === 'string' && patch.text.trim()) {
|
|
223
402
|
entry.text = patch.text.trim().slice(0, 4000);
|
|
403
|
+
entry.mentions = mentionsUnion(entry);
|
|
224
404
|
}
|
|
225
405
|
await saveCommentsForFile(file, list);
|
|
226
406
|
onCommentsChanged(file);
|
|
@@ -662,6 +842,9 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
|
|
|
662
842
|
commentsAdd,
|
|
663
843
|
commentsPatch,
|
|
664
844
|
commentsDelete,
|
|
845
|
+
commentsAddReply,
|
|
846
|
+
gitCommitters,
|
|
847
|
+
parseMentions,
|
|
665
848
|
loadCanvasState,
|
|
666
849
|
saveCanvasState,
|
|
667
850
|
loadCanvasMeta,
|
|
@@ -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,
|
|
@@ -469,17 +470,89 @@ function CanvasRouter({
|
|
|
469
470
|
setHoverEl(null);
|
|
470
471
|
},
|
|
471
472
|
onDropComment: ({ clientX, clientY }) => {
|
|
472
|
-
|
|
473
|
-
|
|
473
|
+
// First try deep mode — preferred when the user clicks exactly on
|
|
474
|
+
// a stamped element. When the deep hit lands on an element with
|
|
475
|
+
// `pointer-events: none` (decorative <svg> children, overlay icons),
|
|
476
|
+
// elementFromPoint propagates past it and `resolveHoverTarget`
|
|
477
|
+
// returns null because the next-closest hit is `.dc-artboard-body`
|
|
478
|
+
// itself.
|
|
479
|
+
let target = resolveHoverTarget(document, clientX, clientY, { deep: true });
|
|
480
|
+
if (!target) target = resolveHoverTarget(document, clientX, clientY, { deep: false });
|
|
481
|
+
// Phase 6 fallback — when both resolveHoverTarget passes bail (the
|
|
482
|
+
// `hit === bodyEl` early-exit triggers on `pointer-events: none`
|
|
483
|
+
// decorations), enumerate every element under the click point and
|
|
484
|
+
// climb the first one that has `data-cd-id`. This is how clicks on
|
|
485
|
+
// SVG logos / icon glyphs land on the actual stamped wrapper.
|
|
486
|
+
if (!target && typeof document.elementsFromPoint === 'function') {
|
|
487
|
+
const stack = document.elementsFromPoint(clientX, clientY);
|
|
488
|
+
for (const candidate of stack) {
|
|
489
|
+
const stamped = (candidate as Element).closest?.('[data-cd-id]') as HTMLElement | null;
|
|
490
|
+
if (!stamped) continue;
|
|
491
|
+
if (!stamped.closest('.dc-artboard-body')) continue;
|
|
492
|
+
const artboardEl = stamped.closest('[data-dc-screen]');
|
|
493
|
+
target = {
|
|
494
|
+
el: stamped,
|
|
495
|
+
cdId: stamped.getAttribute('data-cd-id'),
|
|
496
|
+
artboardId: artboardEl?.getAttribute('data-dc-screen') ?? null,
|
|
497
|
+
};
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (!target) {
|
|
502
|
+
// Floating comment fallback — no element anchor, just a click
|
|
503
|
+
// point. The overlay still renders a pin at the stored bounds.
|
|
504
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
505
|
+
const floatingSel: Selection = {
|
|
506
|
+
file: deriveFile(),
|
|
507
|
+
id: undefined,
|
|
508
|
+
selector: '',
|
|
509
|
+
artboardId: null,
|
|
510
|
+
tag: '',
|
|
511
|
+
classes: '',
|
|
512
|
+
text: '',
|
|
513
|
+
dom_path: [],
|
|
514
|
+
bounds: { x: clientX - 12, y: clientY - 12, w: 24, h: 24 },
|
|
515
|
+
html: '',
|
|
516
|
+
};
|
|
517
|
+
try {
|
|
518
|
+
document.dispatchEvent(
|
|
519
|
+
new CustomEvent('cm:open-composer', {
|
|
520
|
+
detail: { selection: floatingSel, clientX, clientY },
|
|
521
|
+
})
|
|
522
|
+
);
|
|
523
|
+
} catch {
|
|
524
|
+
/* ignore */
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
window.parent.postMessage({ dgn: 'comment-compose', selection: floatingSel }, '*');
|
|
528
|
+
} catch {
|
|
529
|
+
/* parent detached */
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
474
533
|
const sel = hoverTargetToSelection(target);
|
|
475
534
|
// Commit the target to the selection set so the halo persists while
|
|
476
|
-
// the
|
|
477
|
-
// - submit / cancel on the composer (
|
|
535
|
+
// the composer is open. The user clears by:
|
|
536
|
+
// - submit / cancel on the composer (overlay dispatches force-clear)
|
|
478
537
|
// - pressing Esc inside the canvas (router's onEscape → clear)
|
|
479
538
|
// - clicking another element in comment mode (this handler runs
|
|
480
539
|
// again and replaces)
|
|
481
540
|
selSet.replace(sel);
|
|
482
|
-
if (typeof window === 'undefined') return;
|
|
541
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
542
|
+
// Phase 6 — open the in-place composer inside the iframe at the click
|
|
543
|
+
// point. Custom event is iframe-local so the overlay can subscribe
|
|
544
|
+
// without round-tripping through the parent shell.
|
|
545
|
+
try {
|
|
546
|
+
document.dispatchEvent(
|
|
547
|
+
new CustomEvent('cm:open-composer', {
|
|
548
|
+
detail: { selection: sel, clientX, clientY },
|
|
549
|
+
})
|
|
550
|
+
);
|
|
551
|
+
} catch {
|
|
552
|
+
/* CustomEvent absent — fall through to legacy parent path */
|
|
553
|
+
}
|
|
554
|
+
// Still post to parent for back-compat with any legacy `.html` mocks
|
|
555
|
+
// whose inspector script consumes `comment-compose`.
|
|
483
556
|
try {
|
|
484
557
|
window.parent.postMessage({ dgn: 'comment-compose', selection: sel }, '*');
|
|
485
558
|
} catch {
|
|
@@ -492,6 +565,7 @@ function CanvasRouter({
|
|
|
492
565
|
return (
|
|
493
566
|
<>
|
|
494
567
|
{children}
|
|
568
|
+
<CommentsOverlay />
|
|
495
569
|
<AnnotationsLayer />
|
|
496
570
|
<ToolPalette />
|
|
497
571
|
<HoverHalo el={hoverEl} />
|