@1agh/maude 0.22.2 → 0.24.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 +34 -1
- package/cli/bin/maude.mjs +3 -0
- package/cli/commands/cache.mjs +181 -0
- package/cli/commands/cache.test.mjs +166 -0
- package/cli/commands/design-link.test.mjs +84 -2
- package/cli/commands/design.mjs +95 -4
- package/cli/commands/design.test.mjs +56 -0
- package/cli/commands/help.mjs +34 -0
- package/cli/commands/hub.mjs +255 -30
- package/cli/commands/hub.test.mjs +135 -10
- package/cli/commands/init.mjs +3 -0
- package/cli/commands/preflight.mjs +11 -0
- package/cli/commands/preflight.test.mjs +46 -0
- package/cli/commands/scenario-report.mjs +45 -0
- package/cli/lib/cache.mjs +407 -0
- package/cli/lib/cache.test.mjs +303 -0
- package/cli/lib/design-link.mjs +222 -11
- package/cli/lib/flow-design-integration.test.mjs +165 -0
- package/cli/lib/gitignore-block.mjs +90 -0
- package/cli/lib/gitignore-block.test.mjs +123 -0
- package/cli/lib/hubs-config.mjs +42 -4
- package/cli/lib/plugin-cli-reachability.test.mjs +56 -0
- package/cli/lib/preflight.mjs +41 -10
- package/package.json +8 -8
- package/plugins/design/dev-server/ai-banner.tsx +2 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +349 -112
- package/plugins/design/dev-server/annotations-layer.tsx +906 -137
- package/plugins/design/dev-server/api.ts +109 -4
- package/plugins/design/dev-server/bin/preflight.sh +21 -10
- package/plugins/design/dev-server/bin/prep.sh +211 -0
- package/plugins/design/dev-server/bin/scenario-report.mjs +209 -0
- package/plugins/design/dev-server/bin/screenshot.sh +18 -1
- package/plugins/design/dev-server/bin/smoke.sh +94 -11
- package/plugins/design/dev-server/build.ts +69 -3
- package/plugins/design/dev-server/canvas-comment-mount.tsx +384 -0
- package/plugins/design/dev-server/canvas-cursors.ts +125 -0
- package/plugins/design/dev-server/canvas-icons.tsx +130 -0
- package/plugins/design/dev-server/canvas-lib.tsx +47 -27
- package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
- package/plugins/design/dev-server/canvas-shell.tsx +358 -245
- package/plugins/design/dev-server/client/app.jsx +191 -21
- package/plugins/design/dev-server/client/styles/3-shell.css +10 -0
- package/plugins/design/dev-server/collab/awareness-bridge.ts +77 -0
- package/plugins/design/dev-server/collab/index.ts +87 -9
- package/plugins/design/dev-server/collab/persistence.ts +34 -3
- package/plugins/design/dev-server/collab/registry.ts +131 -2
- package/plugins/design/dev-server/collab/room.ts +21 -8
- package/plugins/design/dev-server/config.schema.json +20 -0
- package/plugins/design/dev-server/context-menu.tsx +167 -23
- package/plugins/design/dev-server/context.ts +31 -0
- package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
- package/plugins/design/dev-server/dist/client.bundle.js +144 -22
- package/plugins/design/dev-server/dist/comment-mount.js +1868 -0
- package/plugins/design/dev-server/dist/styles.css +16 -0
- package/plugins/design/dev-server/dom-selection.ts +156 -0
- package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
- package/plugins/design/dev-server/export-dialog.tsx +19 -17
- package/plugins/design/dev-server/fs-watch.ts +1 -0
- package/plugins/design/dev-server/hmr-broadcast.ts +51 -20
- package/plugins/design/dev-server/http.ts +260 -20
- package/plugins/design/dev-server/input-router.tsx +125 -64
- package/plugins/design/dev-server/participants-chrome.tsx +10 -10
- package/plugins/design/dev-server/server.ts +141 -1
- package/plugins/design/dev-server/sync/agent.ts +418 -0
- package/plugins/design/dev-server/sync/atomic-write.ts +103 -0
- package/plugins/design/dev-server/sync/codec.ts +324 -0
- package/plugins/design/dev-server/sync/connection-state.ts +203 -0
- package/plugins/design/dev-server/sync/echo-guard.ts +108 -0
- package/plugins/design/dev-server/sync/fs-mirror.ts +160 -0
- package/plugins/design/dev-server/sync/hubs-config.ts +87 -0
- package/plugins/design/dev-server/sync/index.ts +918 -0
- package/plugins/design/dev-server/sync/materialize.ts +62 -0
- package/plugins/design/dev-server/sync/migrate-seed.ts +163 -0
- package/plugins/design/dev-server/sync/origins.ts +57 -0
- package/plugins/design/dev-server/sync/projection.ts +368 -0
- package/plugins/design/dev-server/sync/status.ts +115 -0
- package/plugins/design/dev-server/sync/untrusted.ts +153 -0
- package/plugins/design/dev-server/test/_helpers.ts +6 -2
- package/plugins/design/dev-server/test/annotations-layer.test.ts +231 -0
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +276 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +73 -0
- package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +76 -0
- package/plugins/design/dev-server/test/collab-awareness-bridge.test.ts +223 -0
- package/plugins/design/dev-server/test/collab-reseed-guard.test.ts +78 -0
- package/plugins/design/dev-server/test/collab-room.test.ts +21 -10
- package/plugins/design/dev-server/test/comment-mount.test.ts +87 -0
- package/plugins/design/dev-server/test/csp-canvas-shell.test.ts +46 -0
- package/plugins/design/dev-server/test/fixtures/phase-20-annotations.svg +1 -0
- package/plugins/design/dev-server/test/hmr-classify.test.ts +70 -0
- package/plugins/design/dev-server/test/input-router.test.ts +21 -0
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +100 -0
- package/plugins/design/dev-server/test/shared-doc-convergence.test.ts +265 -0
- package/plugins/design/dev-server/test/shared-doc-foundation.test.ts +63 -0
- package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +160 -0
- package/plugins/design/dev-server/test/shared-doc-projection.test.ts +238 -0
- package/plugins/design/dev-server/test/sync-agent.test.ts +278 -0
- package/plugins/design/dev-server/test/sync-atomic-write.test.ts +63 -0
- package/plugins/design/dev-server/test/sync-codec.test.ts +165 -0
- package/plugins/design/dev-server/test/sync-connection-state.test.ts +146 -0
- package/plugins/design/dev-server/test/sync-echo-guard.test.ts +96 -0
- package/plugins/design/dev-server/test/sync-fs-mirror.test.ts +182 -0
- package/plugins/design/dev-server/test/sync-hardening.test.ts +520 -0
- package/plugins/design/dev-server/test/sync-hubs-config.test.ts +130 -0
- package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
- package/plugins/design/dev-server/test/sync-runtime.test.ts +812 -0
- package/plugins/design/dev-server/test/sync-status.test.ts +80 -0
- package/plugins/design/dev-server/test/sync-untrusted.test.ts +104 -0
- package/plugins/design/dev-server/test/use-collab.test.ts +0 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +38 -10
- package/plugins/design/dev-server/tool-palette.tsx +18 -16
- package/plugins/design/dev-server/undo-hud.tsx +4 -4
- package/plugins/design/dev-server/undo-stack.ts +20 -4
- package/plugins/design/dev-server/use-annotation-resize.tsx +16 -5
- package/plugins/design/dev-server/use-collab.tsx +157 -13
- package/plugins/design/dev-server/use-selection-set.tsx +12 -0
- package/plugins/design/dev-server/use-tool-mode.tsx +27 -12
- package/plugins/design/dev-server/ws.ts +40 -1
- package/plugins/design/templates/_shell.html +63 -5
- package/plugins/design/templates/canvas.tsx.template +13 -0
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +15 -7
- package/plugins/flow/.claude-plugin/config.schema.json +5 -0
- package/plugins/flow/templates/ai-skeleton/README.md +1 -1
- package/plugins/flow/templates/ai-skeleton/gitignore +10 -0
- package/plugins/flow/templates/ai-skeleton/scenarios/README.md +3 -1
- package/plugins/flow/templates/ai-skeleton/workflows.config.json +2 -1
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
# [--include-system 0|1] (default 1)
|
|
11
11
|
# [--timeout <secs>] (default 8)
|
|
12
12
|
# [--engine auto|agent-browser|playwright]
|
|
13
|
+
# [--changed-only] (default off — screenshot only canvases changed
|
|
14
|
+
# since the last smoke run; escalates to the full
|
|
15
|
+
# set when dev-server / canvas-lib / templates changed)
|
|
13
16
|
#
|
|
14
17
|
# Reads:
|
|
15
18
|
# $DESIGN_ROOT/_server.json (must exist — caller runs server-up.sh first)
|
|
@@ -31,6 +34,7 @@ OUT_DIR=""
|
|
|
31
34
|
INCLUDE_SYSTEM=1
|
|
32
35
|
TIMEOUT=8
|
|
33
36
|
ENGINE="auto"
|
|
37
|
+
CHANGED_ONLY=0
|
|
34
38
|
|
|
35
39
|
while [ $# -gt 0 ]; do
|
|
36
40
|
case "$1" in
|
|
@@ -39,8 +43,9 @@ while [ $# -gt 0 ]; do
|
|
|
39
43
|
--include-system) INCLUDE_SYSTEM="$2"; shift 2 ;;
|
|
40
44
|
--timeout) TIMEOUT="$2"; shift 2 ;;
|
|
41
45
|
--engine) ENGINE="$2"; shift 2 ;;
|
|
46
|
+
--changed-only) CHANGED_ONLY=1; shift ;;
|
|
42
47
|
--help|-h)
|
|
43
|
-
sed -n '2,
|
|
48
|
+
sed -n '2,30p' "$0" | sed 's/^# \?//'
|
|
44
49
|
exit 0
|
|
45
50
|
;;
|
|
46
51
|
*)
|
|
@@ -73,6 +78,10 @@ if [ -z "$OUT_DIR" ]; then
|
|
|
73
78
|
OUT_DIR="$DESIGN_ROOT/_history/_smoke/$TS"
|
|
74
79
|
fi
|
|
75
80
|
mkdir -p "$OUT_DIR"
|
|
81
|
+
# agent-browser ignores RELATIVE screenshot paths (writes to ~/.agent-browser/tmp
|
|
82
|
+
# instead), which silently strands every PNG. Canonicalize to absolute so the
|
|
83
|
+
# captured evidence actually lands in OUT_DIR. See DDR-021.
|
|
84
|
+
OUT_DIR="$(cd "$OUT_DIR" && pwd)"
|
|
76
85
|
|
|
77
86
|
# ---------- engine resolution ----------
|
|
78
87
|
if [ "$ENGINE" = "auto" ]; then
|
|
@@ -107,6 +116,57 @@ COUNT=$(printf '%s' "$CANVASES" | grep -c .)
|
|
|
107
116
|
[ "$COUNT" -gt 0 ] || { echo "smoke.sh: no canvases found (ui/*.tsx or system/*/preview/*.tsx)" >&2; exit 1; }
|
|
108
117
|
echo "→ found $COUNT canvases" >&2
|
|
109
118
|
|
|
119
|
+
# ---------- --changed-only incremental filter (Phase C / DDR-061) ----------
|
|
120
|
+
# Default smoke screenshots every canvas. --changed-only narrows to the canvases
|
|
121
|
+
# touched since the last recorded smoke run, UNLESS the diff touches an
|
|
122
|
+
# "everything could break" shape (dev-server, canvas-lib, canvas templates) — then
|
|
123
|
+
# it escalates back to the full set. Correctness > speed: no baseline, no git, or
|
|
124
|
+
# an escalation trigger all fall back to the full set.
|
|
125
|
+
MARKER="$DESIGN_ROOT/_history/_smoke/.last-smoke.json"
|
|
126
|
+
if [ "$CHANGED_ONLY" = "1" ]; then
|
|
127
|
+
LAST_SHA=""
|
|
128
|
+
if [ -f "$MARKER" ] && command -v jq >/dev/null 2>&1; then
|
|
129
|
+
LAST_SHA=$(jq -r '.sha // empty' "$MARKER" 2>/dev/null)
|
|
130
|
+
fi
|
|
131
|
+
if ! command -v git >/dev/null 2>&1 || ! git -C "$REPO" rev-parse HEAD >/dev/null 2>&1; then
|
|
132
|
+
echo "→ --changed-only: no git in $REPO — running full set" >&2
|
|
133
|
+
elif [ -z "$LAST_SHA" ]; then
|
|
134
|
+
echo "→ --changed-only: no prior smoke baseline ($MARKER) — running full set" >&2
|
|
135
|
+
else
|
|
136
|
+
# All paths changed between the last smoke and the current working tree
|
|
137
|
+
# (committed + uncommitted), plus untracked canvases.
|
|
138
|
+
CHANGED=$(
|
|
139
|
+
{ git -C "$REPO" diff --name-only "$LAST_SHA" -- 2>/dev/null
|
|
140
|
+
git -C "$REPO" ls-files --others --exclude-standard -- .design/ui .design/system 2>/dev/null
|
|
141
|
+
} | sort -u
|
|
142
|
+
)
|
|
143
|
+
if printf '%s\n' "$CHANGED" | grep -qE 'dev-server/|canvas-lib\.tsx|canvas[^/]*\.tsx\.template'; then
|
|
144
|
+
echo "→ --changed-only: dev-server / canvas-lib / template changed — escalating to FULL set" >&2
|
|
145
|
+
else
|
|
146
|
+
# Keep only canvases whose repo-relative path is in the changed set.
|
|
147
|
+
FILTERED=""
|
|
148
|
+
while IFS= read -r CANVAS; do
|
|
149
|
+
[ -z "$CANVAS" ] && continue
|
|
150
|
+
REL_REPO="${CANVAS#$REPO/}"
|
|
151
|
+
if printf '%s\n' "$CHANGED" | grep -qxF "$REL_REPO"; then
|
|
152
|
+
FILTERED="$FILTERED$CANVAS"$'\n'
|
|
153
|
+
fi
|
|
154
|
+
done <<< "$CANVASES"
|
|
155
|
+
CANVASES=$(printf '%s' "$FILTERED" | sed '/^$/d')
|
|
156
|
+
COUNT=$(printf '%s' "$CANVASES" | grep -c .)
|
|
157
|
+
if [ "$COUNT" -eq 0 ]; then
|
|
158
|
+
echo "→ --changed-only: no canvas .tsx changed since last smoke ($LAST_SHA) — nothing to screenshot" >&2
|
|
159
|
+
# Refresh the marker so the next run diffs from here, then exit clean.
|
|
160
|
+
mkdir -p "$(dirname "$MARKER")"
|
|
161
|
+
printf '{"sha":"%s","ts":"%s","mode":"changed-only-noop"}' \
|
|
162
|
+
"$(git -C "$REPO" rev-parse HEAD 2>/dev/null)" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$MARKER"
|
|
163
|
+
exit 0
|
|
164
|
+
fi
|
|
165
|
+
echo "→ --changed-only: $COUNT changed canvas(es) since $LAST_SHA" >&2
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
fi
|
|
169
|
+
|
|
110
170
|
# ---------- helpers ----------
|
|
111
171
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
112
172
|
SLUG_HELPER="$SCRIPT_DIR/slug.sh"
|
|
@@ -160,6 +220,8 @@ probe_agent_browser() {
|
|
|
160
220
|
for (const s of sel) { const el = document.querySelector(s); if (el && el.innerText) return el.innerText.slice(0, 120); }
|
|
161
221
|
const body = document.body && document.body.innerText || '';
|
|
162
222
|
if (body.startsWith('Error:') || body.startsWith('SyntaxError:') || body.startsWith('ReferenceError:')) return body.slice(0, 120);
|
|
223
|
+
const t = body.trim();
|
|
224
|
+
if (t === 'Not found' || t.startsWith('Forbidden (canvas origin)')) return 'route-error: ' + t.slice(0, 80);
|
|
163
225
|
return '';
|
|
164
226
|
})()" 2>/dev/null)
|
|
165
227
|
# Strip wrapping quotes that agent-browser eval adds for strings.
|
|
@@ -172,16 +234,24 @@ probe_agent_browser() {
|
|
|
172
234
|
return
|
|
173
235
|
fi
|
|
174
236
|
|
|
237
|
+
# The PNG IS the evidence (DDR-021). A missing/empty file means agent-browser
|
|
238
|
+
# never wrote it (relative path, capture crash, …) — that is a FAILURE, not an
|
|
239
|
+
# OK. The old code fell straight through to "OK" here, which is exactly how a
|
|
240
|
+
# broken canvas route ("Not found" 404 + no PNG) masqueraded as 43/43 green.
|
|
241
|
+
if [ ! -s "$out_png" ]; then
|
|
242
|
+
echo "ERROR"
|
|
243
|
+
echo "screenshot-not-written"
|
|
244
|
+
echo "$out_png"
|
|
245
|
+
return
|
|
246
|
+
fi
|
|
175
247
|
# PNG size sanity — < 2 KB usually means blank background only.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return
|
|
184
|
-
fi
|
|
248
|
+
local size
|
|
249
|
+
size=$(wc -c < "$out_png" 2>/dev/null | tr -d ' ')
|
|
250
|
+
if [ -n "$size" ] && [ "$size" -lt 2048 ]; then
|
|
251
|
+
echo "BLANK"
|
|
252
|
+
echo "png-${size}B"
|
|
253
|
+
echo "$out_png"
|
|
254
|
+
return
|
|
185
255
|
fi
|
|
186
256
|
|
|
187
257
|
echo "OK"
|
|
@@ -211,7 +281,11 @@ while IFS= read -r CANVAS; do
|
|
|
211
281
|
N=$((N + 1))
|
|
212
282
|
REL="${CANVAS#$DESIGN_ROOT/}"
|
|
213
283
|
REL_ENC=$(urlencode_path "$REL")
|
|
214
|
-
|
|
284
|
+
# Canvases render through the canvas shell, not the bare path. The bare
|
|
285
|
+
# `/<rel>` route 404s when the canvas-origin sandbox is on (default since
|
|
286
|
+
# phase-9.1) — only `/_canvas-shell.html?canvas=<rel>` mounts the canvas
|
|
287
|
+
# (works in both split-on and legacy same-origin modes).
|
|
288
|
+
URL="http://localhost:$PORT/_canvas-shell.html?canvas=$REL_ENC"
|
|
215
289
|
SLUG=$(bash "$SLUG_HELPER" "$REL" 2>/dev/null || printf '%s' "$REL" | tr '/ ' '__' | tr '[:upper:]' '[:lower:]')
|
|
216
290
|
OUT_PNG="$OUT_DIR/$SLUG.png"
|
|
217
291
|
|
|
@@ -264,6 +338,15 @@ done <<< "$CANVASES"
|
|
|
264
338
|
echo "→ report: $MD" >&2
|
|
265
339
|
echo "→ tsv: $TSV" >&2
|
|
266
340
|
|
|
341
|
+
# Record this run as the baseline for the next --changed-only diff (Phase C / DDR-061).
|
|
342
|
+
if command -v git >/dev/null 2>&1 && git -C "$REPO" rev-parse HEAD >/dev/null 2>&1; then
|
|
343
|
+
mkdir -p "$DESIGN_ROOT/_history/_smoke"
|
|
344
|
+
printf '{"sha":"%s","ts":"%s","mode":"%s","count":%d,"failed":%d}' \
|
|
345
|
+
"$(git -C "$REPO" rev-parse HEAD 2>/dev/null)" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
346
|
+
"$([ "$CHANGED_ONLY" = "1" ] && echo changed-only || echo full)" "$COUNT" "$FAILED" \
|
|
347
|
+
> "$DESIGN_ROOT/_history/_smoke/.last-smoke.json"
|
|
348
|
+
fi
|
|
349
|
+
|
|
267
350
|
if [ $FAILED -gt 0 ]; then
|
|
268
351
|
echo "✗ smoke: $FAILED / $COUNT canvases failed" >&2
|
|
269
352
|
exit 3
|
|
@@ -114,6 +114,58 @@ async function buildClient(): Promise<{ outBytes: number; outPath: string }> {
|
|
|
114
114
|
return { outBytes: out.size, outPath };
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// ---------- (a.5) Comment mount layer (shell-owned comments) ----------
|
|
118
|
+
//
|
|
119
|
+
// canvas-comment-mount.tsx → dist/comment-mount.js. Loaded by _shell.html via
|
|
120
|
+
// a `<script type="module">`; its `mountCanvas(...)` wraps any canvas default
|
|
121
|
+
// export in the lite comment provider tree. React + the other canvas-runtime
|
|
122
|
+
// packages are EXTERNAL (bare specifiers resolved by _shell.html's importmap)
|
|
123
|
+
// so this bundle shares the single React/yjs singletons with the canvas
|
|
124
|
+
// module — inlining a second React would break hooks ("invalid hook call").
|
|
125
|
+
|
|
126
|
+
const COMMENT_MOUNT_EXTERNALS = [
|
|
127
|
+
'react',
|
|
128
|
+
'react-dom',
|
|
129
|
+
'react-dom/client',
|
|
130
|
+
'react/jsx-runtime',
|
|
131
|
+
'react/jsx-dev-runtime',
|
|
132
|
+
'yjs',
|
|
133
|
+
'y-protocols/sync',
|
|
134
|
+
'y-protocols/awareness',
|
|
135
|
+
'lib0/decoding',
|
|
136
|
+
'lib0/encoding',
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
async function buildCommentMount(): Promise<{ outBytes: number; outPath: string }> {
|
|
140
|
+
ensureDist();
|
|
141
|
+
const outPath = join(DIST, 'comment-mount.js');
|
|
142
|
+
const result = await Bun.build({
|
|
143
|
+
entrypoints: [join(ROOT, 'canvas-comment-mount.tsx')],
|
|
144
|
+
outdir: DIST,
|
|
145
|
+
target: 'browser',
|
|
146
|
+
format: 'esm',
|
|
147
|
+
naming: 'comment-mount.js',
|
|
148
|
+
external: COMMENT_MOUNT_EXTERNALS,
|
|
149
|
+
minify: MODE === 'release',
|
|
150
|
+
sourcemap: MODE === 'dev' ? 'inline' : 'none',
|
|
151
|
+
define: {
|
|
152
|
+
// ALWAYS production React — like canvas-build.ts. React is external and
|
|
153
|
+
// resolves through the importmap to the dev-server's PRODUCTION runtime
|
|
154
|
+
// bundles, where `react/jsx-dev-runtime`'s `jsxDEV` is a no-op (undefined
|
|
155
|
+
// at call time). Emitting dev jsx (`jsxDEV`) against a production runtime
|
|
156
|
+
// throws "jsxDEV is not a function". Both halves must agree on the JSX
|
|
157
|
+
// flavour — production jsx (`react/jsx-runtime`) is the contract.
|
|
158
|
+
'process.env.NODE_ENV': '"production"',
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
if (!result.success) {
|
|
162
|
+
const messages = result.logs.map((l) => l.message ?? String(l)).join('\n');
|
|
163
|
+
throw new Error(`Comment-mount build failed:\n${messages}`);
|
|
164
|
+
}
|
|
165
|
+
const out = Bun.file(outPath);
|
|
166
|
+
return { outBytes: out.size, outPath };
|
|
167
|
+
}
|
|
168
|
+
|
|
117
169
|
// ---------- (b) CSS bundle (Lightning CSS) ----------
|
|
118
170
|
|
|
119
171
|
async function buildCss(): Promise<{ outBytes: number; outPath: string }> {
|
|
@@ -325,6 +377,7 @@ async function buildServerBinary(target: PlatformTarget): Promise<{ outPath: str
|
|
|
325
377
|
|
|
326
378
|
async function watch() {
|
|
327
379
|
await buildClient();
|
|
380
|
+
await buildCommentMount();
|
|
328
381
|
await buildCss();
|
|
329
382
|
console.log('[build:watch] initial build complete; watching client/ + server source...');
|
|
330
383
|
|
|
@@ -396,10 +449,16 @@ async function main() {
|
|
|
396
449
|
`[build] client.bundle.js ${client.outBytes.toLocaleString()} B (${(t1 - t0).toFixed(0)} ms)`
|
|
397
450
|
);
|
|
398
451
|
|
|
452
|
+
const commentMount = await buildCommentMount();
|
|
453
|
+
const t1b = performance.now();
|
|
454
|
+
console.log(
|
|
455
|
+
`[build] comment-mount.js ${commentMount.outBytes.toLocaleString()} B (${(t1b - t1).toFixed(0)} ms)`
|
|
456
|
+
);
|
|
457
|
+
|
|
399
458
|
const css = await buildCss();
|
|
400
459
|
const t2 = performance.now();
|
|
401
460
|
console.log(
|
|
402
|
-
`[build] styles.css ${css.outBytes.toLocaleString()} B (${(t2 -
|
|
461
|
+
`[build] styles.css ${css.outBytes.toLocaleString()} B (${(t2 - t1b).toFixed(0)} ms)`
|
|
403
462
|
);
|
|
404
463
|
|
|
405
464
|
// Pre-built runtime bundles — ship to disk so /_canvas-runtime/* never
|
|
@@ -415,7 +474,7 @@ async function main() {
|
|
|
415
474
|
// on-disk artifacts against .min-sizes.json after the build.
|
|
416
475
|
if (process.env.MAUDE_SKIP_RUNTIME_BUILD === '1') {
|
|
417
476
|
console.log(
|
|
418
|
-
|
|
477
|
+
'[build] dist/runtime/*.js SKIPPED (MAUDE_SKIP_RUNTIME_BUILD=1 — using committed pre-built)'
|
|
419
478
|
);
|
|
420
479
|
} else {
|
|
421
480
|
const runtime = await buildRuntimeBundles();
|
|
@@ -444,4 +503,11 @@ if (import.meta.main) {
|
|
|
444
503
|
await main();
|
|
445
504
|
}
|
|
446
505
|
|
|
447
|
-
export {
|
|
506
|
+
export {
|
|
507
|
+
buildClient,
|
|
508
|
+
buildCommentMount,
|
|
509
|
+
buildCss,
|
|
510
|
+
buildServerBinary,
|
|
511
|
+
PLATFORM_MATRIX,
|
|
512
|
+
type PlatformTarget,
|
|
513
|
+
};
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file canvas-comment-mount.tsx — shell-owned comment layer + mountCanvas
|
|
3
|
+
* @scope plugins/design/dev-server/canvas-comment-mount.tsx
|
|
4
|
+
* @purpose The canvas mount harness (`_shell.html`) calls `mountCanvas`
|
|
5
|
+
* instead of rendering the canvas default-export raw. mountCanvas
|
|
6
|
+
* wraps ANY default export — a `DesignCanvas` UI canvas OR a bare
|
|
7
|
+
* DS specimen — in a LITE comment provider tree so the in-place
|
|
8
|
+
* comment tool works on every mounted surface.
|
|
9
|
+
*
|
|
10
|
+
* Why a single shell-owned layer (DDR — see plan §"Key decision"):
|
|
11
|
+
* - Comments used to be mounted only by `DesignCanvas` (ToolProvider +
|
|
12
|
+
* SelectionSetProvider + CommentsOverlay + the onDropComment router
|
|
13
|
+
* branch). Bare specimens (`system/<ds>/preview/*.tsx`) never render
|
|
14
|
+
* DesignCanvas, so they had no comment tool at all.
|
|
15
|
+
* - Hoisting the comment subsystem here makes it universal. `DesignCanvas`
|
|
16
|
+
* becomes a CONSUMER of the shell-provided ToolProvider / SelectionSet /
|
|
17
|
+
* CommentsOverlay (via MaybeToolProvider / MaybeSelectionSetProvider and
|
|
18
|
+
* by dropping its own <CommentsOverlay/>).
|
|
19
|
+
*
|
|
20
|
+
* Coexistence with the UI-canvas router: this layer's input router is an
|
|
21
|
+
* ANCESTOR capture-listener over the canvas. On a UI canvas, `CanvasShell`
|
|
22
|
+
* still runs its OWN router (hover / select / context-menu / undo). To avoid
|
|
23
|
+
* swallowing those gestures, this router passes a narrow `claimableActions`
|
|
24
|
+
* allowlist — `drop-comment` / `tool` / `escape` / `hover`. `hover` never
|
|
25
|
+
* preventDefaults so the inner router's halo is unaffected; everything else
|
|
26
|
+
* (select / context-menu / undo) propagates untouched to the inner router.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
type ComponentType,
|
|
31
|
+
type ReactNode,
|
|
32
|
+
createElement,
|
|
33
|
+
useEffect,
|
|
34
|
+
useMemo,
|
|
35
|
+
useRef,
|
|
36
|
+
useState,
|
|
37
|
+
} from 'react';
|
|
38
|
+
import { createRoot } from 'react-dom/client';
|
|
39
|
+
|
|
40
|
+
import { CommentsOverlay } from './comments-overlay.tsx';
|
|
41
|
+
import { deriveFile, hoverTargetToSelection } from './dom-selection.ts';
|
|
42
|
+
import { type RouterAction, resolveHoverTarget, useInputRouter } from './input-router.tsx';
|
|
43
|
+
import {
|
|
44
|
+
MaybeSelectionSetProvider,
|
|
45
|
+
type Selection,
|
|
46
|
+
useSelectionSet,
|
|
47
|
+
} from './use-selection-set.tsx';
|
|
48
|
+
import { MaybeToolProvider, useToolMode } from './use-tool-mode.tsx';
|
|
49
|
+
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
// Cursor CSS for the generic mount host. canvas-lib's HALO_CSS keys comment-
|
|
52
|
+
// mode cursors on `.dc-canvas[data-active-tool]`, which is absent in a bare
|
|
53
|
+
// specimen. We mirror the comment-tool cursor keyed on the mount host's
|
|
54
|
+
// `data-active-tool` attribute so the affordance ships even with no canvas-lib
|
|
55
|
+
// CSS present. The host is `display: contents` (so specimen layout is byte-
|
|
56
|
+
// identical) — a `display:contents` box can't paint a cursor, so the rule
|
|
57
|
+
// targets descendants of the host, not the host box itself.
|
|
58
|
+
|
|
59
|
+
const MC_CURSOR_CSS = `
|
|
60
|
+
[data-mc-host][data-active-tool="comment"] *,
|
|
61
|
+
body[data-active-tool="comment"] * {
|
|
62
|
+
cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'><path d='M3 3 L14 3 L14 11 L8 11 L4 14 L4 11 L3 11 Z' fill='white' stroke='%23111' stroke-width='1' stroke-linejoin='round'/></svg>") 4 4, crosshair !important;
|
|
63
|
+
}
|
|
64
|
+
`.trim();
|
|
65
|
+
|
|
66
|
+
function ensureMountCursorStyles(): void {
|
|
67
|
+
if (typeof document === 'undefined') return;
|
|
68
|
+
if (document.getElementById('mc-cursor-css')) return;
|
|
69
|
+
const s = document.createElement('style');
|
|
70
|
+
s.id = 'mc-cursor-css';
|
|
71
|
+
s.textContent = MC_CURSOR_CSS;
|
|
72
|
+
document.head.appendChild(s);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
// CommentHost — owns the lite comment subsystem: the input router (comment-
|
|
77
|
+
// scoped), the single CommentsOverlay, the comment-mode cursor attribute, and
|
|
78
|
+
// the parent `dgn:'tool-set'` listener (so the outer menubar comment toggle
|
|
79
|
+
// reaches the iframe).
|
|
80
|
+
|
|
81
|
+
// Only these action kinds are claimed by the mount-layer router; the rest
|
|
82
|
+
// propagate to a UI canvas's own router. `tool` + `escape` are also handled by
|
|
83
|
+
// the inner router on UI canvases — idempotent (same shared provider). `hover`
|
|
84
|
+
// is dispatched too (it never preventDefaults, so the inner router's own hover
|
|
85
|
+
// halo on a UI canvas is unaffected) — we only PAINT the mount-layer preview
|
|
86
|
+
// halo on a bare specimen, where there is no inner CanvasShell halo.
|
|
87
|
+
const COMMENT_CLAIMS: ReadonlySet<RouterAction['kind']> = new Set<RouterAction['kind']>([
|
|
88
|
+
'drop-comment',
|
|
89
|
+
'tool',
|
|
90
|
+
'escape',
|
|
91
|
+
'hover',
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
// True when no DesignCanvas/CanvasShell is mounted on this surface — i.e. a
|
|
95
|
+
// bare DS specimen. On a UI canvas (`.dc-canvas` present) the inner shell owns
|
|
96
|
+
// hover-halo painting + `resolveHoverTarget` element anchoring, so the lite
|
|
97
|
+
// layer defers to it.
|
|
98
|
+
function isBareSpecimen(): boolean {
|
|
99
|
+
return typeof document !== 'undefined' && !document.querySelector('.dc-canvas');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Deepest non-chrome element under a point — the comment anchor for a bare
|
|
103
|
+
// specimen (specimens aren't stamped with `data-cd-id` and have no
|
|
104
|
+
// `.dc-artboard-body`, so `resolveHoverTarget` returns null for them).
|
|
105
|
+
function pickSpecimenEl(clientX: number, clientY: number): HTMLElement | null {
|
|
106
|
+
if (typeof document === 'undefined') return null;
|
|
107
|
+
const hit = document.elementFromPoint(clientX, clientY) as HTMLElement | null;
|
|
108
|
+
if (!hit) return null;
|
|
109
|
+
// Never anchor to comment chrome or the document root.
|
|
110
|
+
if (hit.closest('.cm-composer, .cm-thread, .cm-mention-popup, .cm-pin, [data-mc-hover-halo]')) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const tag = hit.tagName;
|
|
114
|
+
if (tag === 'HTML' || tag === 'BODY') return null;
|
|
115
|
+
return hit;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function CommentHost({ children, file }: { children: ReactNode; file: string | undefined }) {
|
|
119
|
+
ensureMountCursorStyles();
|
|
120
|
+
const { tool, setTool } = useToolMode();
|
|
121
|
+
const selSet = useSelectionSet();
|
|
122
|
+
const hostRef = useRef<HTMLDivElement | null>(null);
|
|
123
|
+
// Hover-preview halo target (bare specimens only — see isBareSpecimen).
|
|
124
|
+
const [hoverEl, setHoverEl] = useState<HTMLElement | null>(null);
|
|
125
|
+
|
|
126
|
+
// Latest tool for the router (read at event time, not captured).
|
|
127
|
+
const toolRef = useRef(tool);
|
|
128
|
+
toolRef.current = tool;
|
|
129
|
+
const getActiveTool = useMemo(() => () => toolRef.current, []);
|
|
130
|
+
|
|
131
|
+
// Drop the preview halo whenever we leave comment mode.
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (tool !== 'comment') setHoverEl(null);
|
|
134
|
+
}, [tool]);
|
|
135
|
+
|
|
136
|
+
// Reflect the active tool onto the host (and body, since the host is
|
|
137
|
+
// display:contents and can't carry a paintable cursor). Comment-mode CSS
|
|
138
|
+
// keys off `[data-active-tool="comment"]`.
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
const host = hostRef.current;
|
|
141
|
+
if (host) host.setAttribute('data-active-tool', tool);
|
|
142
|
+
if (typeof document !== 'undefined' && document.body) {
|
|
143
|
+
document.body.setAttribute('data-active-tool', tool);
|
|
144
|
+
}
|
|
145
|
+
return () => {
|
|
146
|
+
host?.removeAttribute('data-active-tool');
|
|
147
|
+
};
|
|
148
|
+
}, [tool]);
|
|
149
|
+
|
|
150
|
+
// Parent `dgn:'tool-set'` — the outer dev-server menubar posts this when the
|
|
151
|
+
// user toggles the comment tool. Mirrors canvas-shell's listener so the
|
|
152
|
+
// toggle reaches bare specimens too (which have no inner shell listener).
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (typeof window === 'undefined') return;
|
|
155
|
+
const onMessage = (e: MessageEvent) => {
|
|
156
|
+
const m = e.data as { dgn?: string; tool?: string } | null;
|
|
157
|
+
if (!m || typeof m !== 'object' || m.dgn !== 'tool-set') return;
|
|
158
|
+
if (typeof m.tool === 'string') setTool(m.tool as never);
|
|
159
|
+
};
|
|
160
|
+
window.addEventListener('message', onMessage);
|
|
161
|
+
return () => window.removeEventListener('message', onMessage);
|
|
162
|
+
}, [setTool]);
|
|
163
|
+
|
|
164
|
+
useInputRouter({
|
|
165
|
+
hostRef,
|
|
166
|
+
getActiveTool,
|
|
167
|
+
claimableActions: COMMENT_CLAIMS,
|
|
168
|
+
callbacks: {
|
|
169
|
+
onHover: ({ clientX, clientY }) => {
|
|
170
|
+
// Paint a preview halo only on bare specimens; a UI canvas's own
|
|
171
|
+
// CanvasShell HoverHalo owns the comment-mode preview there.
|
|
172
|
+
if (toolRef.current !== 'comment' || !isBareSpecimen()) {
|
|
173
|
+
setHoverEl(null);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const el = pickSpecimenEl(clientX, clientY);
|
|
177
|
+
setHoverEl((prev) => (prev === el ? prev : el));
|
|
178
|
+
},
|
|
179
|
+
onTool: ({ tool: t }) => setTool(t),
|
|
180
|
+
onEscape: () => {
|
|
181
|
+
if (toolRef.current !== 'move') setTool('move');
|
|
182
|
+
setHoverEl(null);
|
|
183
|
+
selSet.clear();
|
|
184
|
+
if (typeof window !== 'undefined') {
|
|
185
|
+
try {
|
|
186
|
+
window.parent.postMessage({ dgn: 'force-clear' }, '*');
|
|
187
|
+
} catch {
|
|
188
|
+
/* parent detached */
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
onDropComment: ({ clientX, clientY }) => dropComment(clientX, clientY, selSet, file),
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// `display: contents` keeps the specimen's own flex/grid layout byte-
|
|
197
|
+
// identical — the host box contributes nothing to layout. The fixed-position
|
|
198
|
+
// CommentsOverlay renders fine as a child regardless.
|
|
199
|
+
return (
|
|
200
|
+
<div data-mc-host ref={hostRef} style={{ display: 'contents' }}>
|
|
201
|
+
{children}
|
|
202
|
+
{hoverEl ? <MountHoverHalo el={hoverEl} /> : null}
|
|
203
|
+
<CommentsOverlay />
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
209
|
+
// MountHoverHalo — fixed-position outline tracking the hovered element's screen
|
|
210
|
+
// bounds via rAF. Inline-styled (no dependency on canvas-lib's HALO_CSS, which
|
|
211
|
+
// a bare specimen doesn't load). Mirrors canvas-shell's HoverHalo visually.
|
|
212
|
+
|
|
213
|
+
function MountHoverHalo({ el }: { el: HTMLElement }) {
|
|
214
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
|
215
|
+
const targetRef = useRef<HTMLElement>(el);
|
|
216
|
+
targetRef.current = el;
|
|
217
|
+
const rafRef = useRef<number | null>(null);
|
|
218
|
+
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
const tick = () => {
|
|
221
|
+
rafRef.current = null;
|
|
222
|
+
const div = ref.current;
|
|
223
|
+
const t = targetRef.current;
|
|
224
|
+
if (div && t?.isConnected) {
|
|
225
|
+
const r = t.getBoundingClientRect();
|
|
226
|
+
if (r.width === 0 && r.height === 0) {
|
|
227
|
+
div.style.display = 'none';
|
|
228
|
+
} else {
|
|
229
|
+
div.style.display = 'block';
|
|
230
|
+
div.style.left = `${Math.round(r.left)}px`;
|
|
231
|
+
div.style.top = `${Math.round(r.top)}px`;
|
|
232
|
+
div.style.width = `${Math.round(r.width)}px`;
|
|
233
|
+
div.style.height = `${Math.round(r.height)}px`;
|
|
234
|
+
}
|
|
235
|
+
} else if (div) {
|
|
236
|
+
div.style.display = 'none';
|
|
237
|
+
}
|
|
238
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
239
|
+
};
|
|
240
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
241
|
+
return () => {
|
|
242
|
+
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
|
|
243
|
+
};
|
|
244
|
+
}, []);
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<div
|
|
248
|
+
ref={ref}
|
|
249
|
+
aria-hidden="true"
|
|
250
|
+
data-mc-hover-halo=""
|
|
251
|
+
style={{
|
|
252
|
+
position: 'fixed',
|
|
253
|
+
display: 'none',
|
|
254
|
+
pointerEvents: 'none',
|
|
255
|
+
zIndex: 2147483646,
|
|
256
|
+
border: '2px solid var(--maude-hud-accent, #d63b1f)',
|
|
257
|
+
borderRadius: '3px',
|
|
258
|
+
boxSizing: 'border-box',
|
|
259
|
+
}}
|
|
260
|
+
/>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
265
|
+
// Comment drop — generic path lifted from canvas-shell's onDropComment. Works
|
|
266
|
+
// with or without artboards: deep/shallow resolveHoverTarget → elementsFromPoint
|
|
267
|
+
// data-cd-id climb → floating fallback. Dispatches `cm:open-composer` for the
|
|
268
|
+
// in-iframe overlay + posts `comment-compose` to the parent for legacy mocks.
|
|
269
|
+
|
|
270
|
+
function dropComment(
|
|
271
|
+
clientX: number,
|
|
272
|
+
clientY: number,
|
|
273
|
+
selSet: { replace: (s: Selection) => void },
|
|
274
|
+
file: string | undefined
|
|
275
|
+
): void {
|
|
276
|
+
if (typeof document === 'undefined') return;
|
|
277
|
+
let target = resolveHoverTarget(document, clientX, clientY, { deep: true });
|
|
278
|
+
if (!target) target = resolveHoverTarget(document, clientX, clientY, { deep: false });
|
|
279
|
+
// UI-canvas recovery — when both passes bail on a `pointer-events: none`
|
|
280
|
+
// decoration, enumerate the stack and climb the first `data-cd-id` ancestor
|
|
281
|
+
// inside an artboard body. Skipped on bare specimens, which instead anchor to
|
|
282
|
+
// the exact hovered element below (so the pin matches the hover preview halo).
|
|
283
|
+
if (!target && !isBareSpecimen() && typeof document.elementsFromPoint === 'function') {
|
|
284
|
+
const stack = document.elementsFromPoint(clientX, clientY);
|
|
285
|
+
for (const candidate of stack) {
|
|
286
|
+
const stamped = (candidate as Element).closest?.('[data-cd-id]') as HTMLElement | null;
|
|
287
|
+
if (!stamped) continue;
|
|
288
|
+
if (!stamped.closest('.dc-artboard-body')) continue;
|
|
289
|
+
const artboardEl = stamped.closest('[data-dc-screen]');
|
|
290
|
+
target = {
|
|
291
|
+
el: stamped,
|
|
292
|
+
cdId: stamped.getAttribute('data-cd-id'),
|
|
293
|
+
artboardId: artboardEl?.getAttribute('data-dc-screen') ?? null,
|
|
294
|
+
};
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Bare-specimen anchor — specimens have no `.dc-artboard-body`, so
|
|
300
|
+
// resolveHoverTarget bails. Anchor to the EXACT element under the cursor (the
|
|
301
|
+
// same one pickSpecimenEl highlights for the hover preview), via its own
|
|
302
|
+
// data-cd-id when stamped, else a cssPath selector — so the dropped pin lands
|
|
303
|
+
// on the previewed element instead of floating.
|
|
304
|
+
if (!target && isBareSpecimen()) {
|
|
305
|
+
const el = pickSpecimenEl(clientX, clientY);
|
|
306
|
+
if (el) target = { el, cdId: el.getAttribute('data-cd-id'), artboardId: null };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!target) {
|
|
310
|
+
// Floating comment — no element anchor, just the click point (e.g. a click
|
|
311
|
+
// on empty canvas/specimen dead space). The overlay renders a pin at the
|
|
312
|
+
// stored bounds.
|
|
313
|
+
const floatingSel: Selection = {
|
|
314
|
+
file,
|
|
315
|
+
id: undefined,
|
|
316
|
+
selector: '',
|
|
317
|
+
artboardId: null,
|
|
318
|
+
tag: '',
|
|
319
|
+
classes: '',
|
|
320
|
+
text: '',
|
|
321
|
+
dom_path: [],
|
|
322
|
+
bounds: { x: clientX - 12, y: clientY - 12, w: 24, h: 24 },
|
|
323
|
+
html: '',
|
|
324
|
+
};
|
|
325
|
+
openComposer(floatingSel, clientX, clientY);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const sel = hoverTargetToSelection(target, file);
|
|
330
|
+
selSet.replace(sel);
|
|
331
|
+
openComposer(sel, clientX, clientY);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function openComposer(selection: Selection, clientX: number, clientY: number): void {
|
|
335
|
+
if (typeof document !== 'undefined') {
|
|
336
|
+
try {
|
|
337
|
+
document.dispatchEvent(
|
|
338
|
+
new CustomEvent('cm:open-composer', { detail: { selection, clientX, clientY } })
|
|
339
|
+
);
|
|
340
|
+
} catch {
|
|
341
|
+
/* CustomEvent absent — fall through to parent path */
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (typeof window !== 'undefined') {
|
|
345
|
+
try {
|
|
346
|
+
window.parent.postMessage({ dgn: 'comment-compose', selection }, '*');
|
|
347
|
+
} catch {
|
|
348
|
+
/* parent detached */
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
354
|
+
// mountCanvas — the shell harness entry. Renders the canvas wrapped in the lite
|
|
355
|
+
// comment tree, or bare when comments are disabled (gallery thumbnails pass
|
|
356
|
+
// `commentsEnabled: false` via `?comments=0`).
|
|
357
|
+
|
|
358
|
+
export interface MountCanvasOptions {
|
|
359
|
+
rootEl: HTMLElement;
|
|
360
|
+
/** Canvas file key (designRel-prefixed). Defaults to `deriveFile()`. */
|
|
361
|
+
file?: string;
|
|
362
|
+
/** When false, the comment layer is skipped — the canvas renders raw. */
|
|
363
|
+
commentsEnabled: boolean;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function mountCanvas(Canvas: ComponentType, opts: MountCanvasOptions): void {
|
|
367
|
+
const root = createRoot(opts.rootEl);
|
|
368
|
+
if (!opts.commentsEnabled) {
|
|
369
|
+
root.render(createElement(Canvas));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const file = opts.file ?? deriveFile();
|
|
373
|
+
root.render(
|
|
374
|
+
createElement(
|
|
375
|
+
MaybeToolProvider,
|
|
376
|
+
null,
|
|
377
|
+
createElement(
|
|
378
|
+
MaybeSelectionSetProvider,
|
|
379
|
+
null,
|
|
380
|
+
createElement(CommentHost, { file }, createElement(Canvas))
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
);
|
|
384
|
+
}
|