@1agh/maude 0.23.0 → 0.25.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 +32 -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 +126 -2
- 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 +74 -10
- 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/plugin-cli-reachability.test.mjs +56 -0
- package/cli/lib/preflight.mjs +41 -10
- package/package.json +8 -8
- package/plugins/design/dependencies.json +30 -2
- package/plugins/design/dev-server/ai-banner.tsx +2 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +763 -123
- package/plugins/design/dev-server/annotations-layer.tsx +1624 -208
- package/plugins/design/dev-server/api.ts +123 -4
- package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_html-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pdf-playwright.mjs +25 -8
- package/plugins/design/dev-server/bin/_png-playwright.mjs +20 -20
- package/plugins/design/dev-server/bin/_pptx-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pw-launch.mjs +61 -0
- package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_svg-playwright.mjs +125 -19
- 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 +208 -23
- package/plugins/design/dev-server/canvas-arrowheads.ts +220 -0
- package/plugins/design/dev-server/canvas-comment-mount.tsx +8 -24
- package/plugins/design/dev-server/canvas-cursors.ts +173 -0
- package/plugins/design/dev-server/canvas-icons.tsx +299 -0
- package/plugins/design/dev-server/canvas-lib.tsx +25 -21
- package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
- package/plugins/design/dev-server/canvas-shell.tsx +404 -66
- package/plugins/design/dev-server/client/app.jsx +1231 -416
- package/plugins/design/dev-server/client/styles/3-shell.css +10 -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 +80 -2
- package/plugins/design/dev-server/collab/room.ts +21 -8
- package/plugins/design/dev-server/context-menu.tsx +167 -23
- package/plugins/design/dev-server/context.ts +24 -0
- package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
- package/plugins/design/dev-server/dist/client.bundle.js +364 -29
- package/plugins/design/dev-server/dist/comment-mount.js +78 -33
- package/plugins/design/dev-server/dist/styles.css +16 -0
- package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
- package/plugins/design/dev-server/export-dialog.tsx +208 -18
- package/plugins/design/dev-server/exporters/html.ts +4 -1
- package/plugins/design/dev-server/exporters/index.ts +4 -1
- package/plugins/design/dev-server/exporters/pdf.ts +7 -1
- package/plugins/design/dev-server/exporters/png.ts +21 -2
- package/plugins/design/dev-server/exporters/pptx.ts +330 -201
- package/plugins/design/dev-server/exporters/scope.ts +107 -11
- package/plugins/design/dev-server/exporters/svg.ts +4 -1
- package/plugins/design/dev-server/fs-watch.ts +1 -0
- package/plugins/design/dev-server/http.ts +299 -28
- package/plugins/design/dev-server/input-router.tsx +35 -5
- package/plugins/design/dev-server/participants-chrome.tsx +10 -10
- package/plugins/design/dev-server/server.ts +123 -1
- package/plugins/design/dev-server/sync/agent.ts +95 -0
- package/plugins/design/dev-server/sync/codec.ts +155 -0
- package/plugins/design/dev-server/sync/connection-state.ts +203 -0
- package/plugins/design/dev-server/sync/index.ts +479 -35
- 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-draw-modifiers.test.ts +206 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +311 -0
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +519 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +162 -0
- package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +76 -0
- package/plugins/design/dev-server/test/canvas-route.test.ts +4 -1
- 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/csp-canvas-shell.test.ts +46 -0
- package/plugins/design/dev-server/test/exporters/png.test.ts +24 -1
- package/plugins/design/dev-server/test/exporters/pptx-deck.test.ts +112 -0
- package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +49 -0
- package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
- package/plugins/design/dev-server/test/fixtures/phase-20-annotations.svg +1 -0
- package/plugins/design/dev-server/test/fixtures/phase-21-annotations.svg +1 -0
- package/plugins/design/dev-server/test/input-router.test.ts +32 -4
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +132 -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-connection-state.test.ts +146 -0
- package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
- package/plugins/design/dev-server/test/sync-runtime.test.ts +531 -4
- 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-annotation-resize.test.ts +200 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +39 -13
- package/plugins/design/dev-server/tool-palette.tsx +155 -24
- 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 +219 -61
- package/plugins/design/dev-server/use-tool-mode.tsx +68 -19
- package/plugins/design/dev-server/ws.ts +40 -1
- package/plugins/design/templates/_shell.html +85 -10
- package/plugins/design/templates/canvas.tsx.template +13 -0
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +14 -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
|
@@ -4,12 +4,23 @@
|
|
|
4
4
|
# per-canvas hooks in /design:edit step 7 / /design:new step 9 — typically
|
|
5
5
|
# dev-server infra changes or bulk multi-canvas migrations. See DDR-021.
|
|
6
6
|
#
|
|
7
|
+
# Beyond "does it render", preview specimens are also gated on "does it render
|
|
8
|
+
# STYLED" (DDR-068): a static import-graph lint (every specimen reaches
|
|
9
|
+
# _layout.css — its single CSS entry, which @imports the tokens + _components.css
|
|
10
|
+
# — and every shared preview/_*.css has >=1 importer) plus a runtime
|
|
11
|
+
# computed-style check (the DS token contract `--bg-0` must resolve on the body,
|
|
12
|
+
# else the canvas's import graph lost the token CSS and every var()-driven rule
|
|
13
|
+
# is dead — the specimen mounts with content but renders unstyled).
|
|
14
|
+
#
|
|
7
15
|
# Usage:
|
|
8
16
|
# smoke.sh [--root <repo>]
|
|
9
17
|
# [--out-dir <dir>]
|
|
10
18
|
# [--include-system 0|1] (default 1)
|
|
11
19
|
# [--timeout <secs>] (default 8)
|
|
12
20
|
# [--engine auto|agent-browser|playwright]
|
|
21
|
+
# [--changed-only] (default off — screenshot only canvases changed
|
|
22
|
+
# since the last smoke run; escalates to the full
|
|
23
|
+
# set when dev-server / canvas-lib / templates changed)
|
|
13
24
|
#
|
|
14
25
|
# Reads:
|
|
15
26
|
# $DESIGN_ROOT/_server.json (must exist — caller runs server-up.sh first)
|
|
@@ -24,13 +35,14 @@
|
|
|
24
35
|
# Stdout: one line per canvas, tab-separated:
|
|
25
36
|
# STATUS \t FILE \t SCREENSHOT \t DETAIL
|
|
26
37
|
# Stderr: diagnostic / progress.
|
|
27
|
-
# Exit: 0 = all green / 3 = at least one blank/error / 1 = missing deps / 2 = bad args.
|
|
38
|
+
# Exit: 0 = all green / 3 = at least one blank/error/unstyled/lint-fail / 1 = missing deps / 2 = bad args.
|
|
28
39
|
|
|
29
40
|
REPO=""
|
|
30
41
|
OUT_DIR=""
|
|
31
42
|
INCLUDE_SYSTEM=1
|
|
32
43
|
TIMEOUT=8
|
|
33
44
|
ENGINE="auto"
|
|
45
|
+
CHANGED_ONLY=0
|
|
34
46
|
|
|
35
47
|
while [ $# -gt 0 ]; do
|
|
36
48
|
case "$1" in
|
|
@@ -39,8 +51,9 @@ while [ $# -gt 0 ]; do
|
|
|
39
51
|
--include-system) INCLUDE_SYSTEM="$2"; shift 2 ;;
|
|
40
52
|
--timeout) TIMEOUT="$2"; shift 2 ;;
|
|
41
53
|
--engine) ENGINE="$2"; shift 2 ;;
|
|
54
|
+
--changed-only) CHANGED_ONLY=1; shift ;;
|
|
42
55
|
--help|-h)
|
|
43
|
-
sed -n '2,
|
|
56
|
+
sed -n '2,30p' "$0" | sed 's/^# \?//'
|
|
44
57
|
exit 0
|
|
45
58
|
;;
|
|
46
59
|
*)
|
|
@@ -73,6 +86,10 @@ if [ -z "$OUT_DIR" ]; then
|
|
|
73
86
|
OUT_DIR="$DESIGN_ROOT/_history/_smoke/$TS"
|
|
74
87
|
fi
|
|
75
88
|
mkdir -p "$OUT_DIR"
|
|
89
|
+
# agent-browser ignores RELATIVE screenshot paths (writes to ~/.agent-browser/tmp
|
|
90
|
+
# instead), which silently strands every PNG. Canonicalize to absolute so the
|
|
91
|
+
# captured evidence actually lands in OUT_DIR. See DDR-021.
|
|
92
|
+
OUT_DIR="$(cd "$OUT_DIR" && pwd)"
|
|
76
93
|
|
|
77
94
|
# ---------- engine resolution ----------
|
|
78
95
|
if [ "$ENGINE" = "auto" ]; then
|
|
@@ -107,6 +124,57 @@ COUNT=$(printf '%s' "$CANVASES" | grep -c .)
|
|
|
107
124
|
[ "$COUNT" -gt 0 ] || { echo "smoke.sh: no canvases found (ui/*.tsx or system/*/preview/*.tsx)" >&2; exit 1; }
|
|
108
125
|
echo "→ found $COUNT canvases" >&2
|
|
109
126
|
|
|
127
|
+
# ---------- --changed-only incremental filter (Phase C / DDR-061) ----------
|
|
128
|
+
# Default smoke screenshots every canvas. --changed-only narrows to the canvases
|
|
129
|
+
# touched since the last recorded smoke run, UNLESS the diff touches an
|
|
130
|
+
# "everything could break" shape (dev-server, canvas-lib, canvas templates) — then
|
|
131
|
+
# it escalates back to the full set. Correctness > speed: no baseline, no git, or
|
|
132
|
+
# an escalation trigger all fall back to the full set.
|
|
133
|
+
MARKER="$DESIGN_ROOT/_history/_smoke/.last-smoke.json"
|
|
134
|
+
if [ "$CHANGED_ONLY" = "1" ]; then
|
|
135
|
+
LAST_SHA=""
|
|
136
|
+
if [ -f "$MARKER" ] && command -v jq >/dev/null 2>&1; then
|
|
137
|
+
LAST_SHA=$(jq -r '.sha // empty' "$MARKER" 2>/dev/null)
|
|
138
|
+
fi
|
|
139
|
+
if ! command -v git >/dev/null 2>&1 || ! git -C "$REPO" rev-parse HEAD >/dev/null 2>&1; then
|
|
140
|
+
echo "→ --changed-only: no git in $REPO — running full set" >&2
|
|
141
|
+
elif [ -z "$LAST_SHA" ]; then
|
|
142
|
+
echo "→ --changed-only: no prior smoke baseline ($MARKER) — running full set" >&2
|
|
143
|
+
else
|
|
144
|
+
# All paths changed between the last smoke and the current working tree
|
|
145
|
+
# (committed + uncommitted), plus untracked canvases.
|
|
146
|
+
CHANGED=$(
|
|
147
|
+
{ git -C "$REPO" diff --name-only "$LAST_SHA" -- 2>/dev/null
|
|
148
|
+
git -C "$REPO" ls-files --others --exclude-standard -- .design/ui .design/system 2>/dev/null
|
|
149
|
+
} | sort -u
|
|
150
|
+
)
|
|
151
|
+
if printf '%s\n' "$CHANGED" | grep -qE 'dev-server/|canvas-lib\.tsx|canvas[^/]*\.tsx\.template'; then
|
|
152
|
+
echo "→ --changed-only: dev-server / canvas-lib / template changed — escalating to FULL set" >&2
|
|
153
|
+
else
|
|
154
|
+
# Keep only canvases whose repo-relative path is in the changed set.
|
|
155
|
+
FILTERED=""
|
|
156
|
+
while IFS= read -r CANVAS; do
|
|
157
|
+
[ -z "$CANVAS" ] && continue
|
|
158
|
+
REL_REPO="${CANVAS#$REPO/}"
|
|
159
|
+
if printf '%s\n' "$CHANGED" | grep -qxF "$REL_REPO"; then
|
|
160
|
+
FILTERED="$FILTERED$CANVAS"$'\n'
|
|
161
|
+
fi
|
|
162
|
+
done <<< "$CANVASES"
|
|
163
|
+
CANVASES=$(printf '%s' "$FILTERED" | sed '/^$/d')
|
|
164
|
+
COUNT=$(printf '%s' "$CANVASES" | grep -c .)
|
|
165
|
+
if [ "$COUNT" -eq 0 ]; then
|
|
166
|
+
echo "→ --changed-only: no canvas .tsx changed since last smoke ($LAST_SHA) — nothing to screenshot" >&2
|
|
167
|
+
# Refresh the marker so the next run diffs from here, then exit clean.
|
|
168
|
+
mkdir -p "$(dirname "$MARKER")"
|
|
169
|
+
printf '{"sha":"%s","ts":"%s","mode":"changed-only-noop"}' \
|
|
170
|
+
"$(git -C "$REPO" rev-parse HEAD 2>/dev/null)" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$MARKER"
|
|
171
|
+
exit 0
|
|
172
|
+
fi
|
|
173
|
+
echo "→ --changed-only: $COUNT changed canvas(es) since $LAST_SHA" >&2
|
|
174
|
+
fi
|
|
175
|
+
fi
|
|
176
|
+
fi
|
|
177
|
+
|
|
110
178
|
# ---------- helpers ----------
|
|
111
179
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
112
180
|
SLUG_HELPER="$SCRIPT_DIR/slug.sh"
|
|
@@ -116,6 +184,59 @@ urlencode_path() {
|
|
|
116
184
|
printf '%s' "$1" | sed 's/ /%20/g'
|
|
117
185
|
}
|
|
118
186
|
|
|
187
|
+
# Static import-graph lint (DDR-068). Runs once before render. The dev-server
|
|
188
|
+
# inlines ONLY the CSS a canvas's import graph produces (canvas-build.ts), so a
|
|
189
|
+
# forgotten import is a silently unstyled specimen that still "has content" and
|
|
190
|
+
# sails past the per-canvas render checks below. Asserts, per DS preview dir:
|
|
191
|
+
# (1) every specimen reaches `_layout.css` (direct `import`, or its own css
|
|
192
|
+
# `@import`s it) — _layout.css is the single CSS entry that pulls tokens
|
|
193
|
+
# (colors_and_type.css) + controls (_components.css);
|
|
194
|
+
# (2) every shared `preview/_*.css` has >=1 importer (no orphan partial — the
|
|
195
|
+
# class of bug where `.btn`/`.input` in _components.css loaded NOWHERE).
|
|
196
|
+
# Appends violations to $MD; sets LINT_FAILED. Anchored to real `^import` lines
|
|
197
|
+
# so a specimen that DISPLAYS a css filename in its content isn't a false match.
|
|
198
|
+
LINT_FAILED=0
|
|
199
|
+
lint_specimen_imports() {
|
|
200
|
+
local SQ Q preview tsx base css imp impfile reached n=0
|
|
201
|
+
SQ=$(printf '\047'); Q="[\"$SQ]" # bracket class matching either quote style
|
|
202
|
+
for preview in "$DESIGN_ROOT"/system/*/preview; do
|
|
203
|
+
[ -d "$preview" ] || continue
|
|
204
|
+
# (1) every specimen reaches _layout.css
|
|
205
|
+
for tsx in "$preview"/*.tsx; do
|
|
206
|
+
[ -e "$tsx" ] || continue
|
|
207
|
+
base=$(basename "$tsx"); case "$base" in _*) continue ;; esac
|
|
208
|
+
grep -qE "^[[:space:]]*import[^;]*${Q}[^\"$SQ]*_layout\.css" "$tsx" && continue
|
|
209
|
+
reached=0
|
|
210
|
+
while IFS= read -r imp; do
|
|
211
|
+
impfile="$preview/$(basename "$imp")"
|
|
212
|
+
[ -f "$impfile" ] || continue
|
|
213
|
+
if grep -qE "@import[^;]*_layout\.css" "$impfile"; then reached=1; break; fi
|
|
214
|
+
done < <(grep -oE "import[[:space:]]+${Q}[^\"$SQ]*\.css" "$tsx" 2>/dev/null | grep -oE "[^\"$SQ]*\.css$")
|
|
215
|
+
if [ "$reached" -eq 0 ]; then
|
|
216
|
+
printf 'LINT-FAIL %s — never reaches _layout.css (no DS tokens/layout/components)\n' "${tsx#$DESIGN_ROOT/}" >&2
|
|
217
|
+
printf '| ✗ LINT | `%s` | — | never reaches _layout.css |\n' "${tsx#$DESIGN_ROOT/}" >> "$MD"
|
|
218
|
+
n=$((n + 1))
|
|
219
|
+
fi
|
|
220
|
+
done
|
|
221
|
+
# (2) every shared _*.css partial has an importer
|
|
222
|
+
for css in "$preview"/_*.css; do
|
|
223
|
+
[ -e "$css" ] || continue
|
|
224
|
+
base=$(basename "$css")
|
|
225
|
+
grep -qsE "^[[:space:]]*import[^;]*${Q}[^\"$SQ]*${base}${Q}" "$preview"/*.tsx && continue
|
|
226
|
+
grep -qsE "@import[^;]*${base}" "$preview"/*.css "$preview"/../*.css && continue
|
|
227
|
+
printf 'LINT-FAIL %s — orphan shared partial (no specimen or css imports it)\n' "${css#$DESIGN_ROOT/}" >&2
|
|
228
|
+
printf '| ✗ LINT | `%s` | — | orphan shared partial |\n' "${css#$DESIGN_ROOT/}" >> "$MD"
|
|
229
|
+
n=$((n + 1))
|
|
230
|
+
done
|
|
231
|
+
done
|
|
232
|
+
LINT_FAILED=$n
|
|
233
|
+
if [ "$n" -eq 0 ]; then
|
|
234
|
+
echo "→ import-graph lint: clean" >&2
|
|
235
|
+
else
|
|
236
|
+
echo "✗ import-graph lint: $n violation(s) — see report" >&2
|
|
237
|
+
fi
|
|
238
|
+
}
|
|
239
|
+
|
|
119
240
|
# Probe a canvas via agent-browser. Returns three lines on stdout:
|
|
120
241
|
# <status> one of OK / BLANK / ERROR
|
|
121
242
|
# <detail> short summary string
|
|
@@ -123,6 +244,7 @@ urlencode_path() {
|
|
|
123
244
|
probe_agent_browser() {
|
|
124
245
|
local url="$1"
|
|
125
246
|
local out_png="$2"
|
|
247
|
+
local is_specimen="$3" # 1 → apply the DDR-068 computed-style gate
|
|
126
248
|
|
|
127
249
|
agent-browser open "$url" >/dev/null 2>&1 || { echo "ERROR"; echo "open-failed"; echo ""; return; }
|
|
128
250
|
|
|
@@ -160,6 +282,8 @@ probe_agent_browser() {
|
|
|
160
282
|
for (const s of sel) { const el = document.querySelector(s); if (el && el.innerText) return el.innerText.slice(0, 120); }
|
|
161
283
|
const body = document.body && document.body.innerText || '';
|
|
162
284
|
if (body.startsWith('Error:') || body.startsWith('SyntaxError:') || body.startsWith('ReferenceError:')) return body.slice(0, 120);
|
|
285
|
+
const t = body.trim();
|
|
286
|
+
if (t === 'Not found' || t.startsWith('Forbidden (canvas origin)')) return 'route-error: ' + t.slice(0, 80);
|
|
163
287
|
return '';
|
|
164
288
|
})()" 2>/dev/null)
|
|
165
289
|
# Strip wrapping quotes that agent-browser eval adds for strings.
|
|
@@ -172,16 +296,53 @@ probe_agent_browser() {
|
|
|
172
296
|
return
|
|
173
297
|
fi
|
|
174
298
|
|
|
299
|
+
# The PNG IS the evidence (DDR-021). A missing/empty file means agent-browser
|
|
300
|
+
# never wrote it (relative path, capture crash, …) — that is a FAILURE, not an
|
|
301
|
+
# OK. The old code fell straight through to "OK" here, which is exactly how a
|
|
302
|
+
# broken canvas route ("Not found" 404 + no PNG) masqueraded as 43/43 green.
|
|
303
|
+
if [ ! -s "$out_png" ]; then
|
|
304
|
+
echo "ERROR"
|
|
305
|
+
echo "screenshot-not-written"
|
|
306
|
+
echo "$out_png"
|
|
307
|
+
return
|
|
308
|
+
fi
|
|
175
309
|
# PNG size sanity — < 2 KB usually means blank background only.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
310
|
+
local size
|
|
311
|
+
size=$(wc -c < "$out_png" 2>/dev/null | tr -d ' ')
|
|
312
|
+
if [ -n "$size" ] && [ "$size" -lt 2048 ]; then
|
|
313
|
+
echo "BLANK"
|
|
314
|
+
echo "png-${size}B"
|
|
315
|
+
echo "$out_png"
|
|
316
|
+
return
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
# Computed-style gate (DDR-068) — "mounts with content" ≠ "rendered styled". A
|
|
320
|
+
# specimen whose import graph dropped the token CSS passes every check above
|
|
321
|
+
# (it has text, no error, a >2 KB screenshot) yet every var()-driven rule is
|
|
322
|
+
# dead. Confirm the DS token contract actually resolved at runtime: --bg-0 is
|
|
323
|
+
# defined on :root by colors_and_type.css (DDR-043 name contract) and inherits
|
|
324
|
+
# to <body>, so an empty value means the token CSS never loaded. Scoped to DS
|
|
325
|
+
# preview specimens — ui/ app canvases may legitimately opt out of the tokens.
|
|
326
|
+
if [ "$is_specimen" = "1" ]; then
|
|
327
|
+
local styled
|
|
328
|
+
styled=$(agent-browser eval "(() => {
|
|
329
|
+
const b = document.body; if (!b) return 'unstyled:no-body';
|
|
330
|
+
const cs = getComputedStyle(b);
|
|
331
|
+
const tok = (cs.getPropertyValue('--bg-0').trim() || cs.getPropertyValue('--fg-0').trim() || cs.getPropertyValue('--accent').trim());
|
|
332
|
+
if (tok) return 'styled';
|
|
333
|
+
return 'unstyled:no-tokens(ff=' + ((cs.fontFamily||'').replace(/[\"\\n]/g,'').slice(0,18)) + ')';
|
|
334
|
+
})()" 2>/dev/null)
|
|
335
|
+
styled=$(printf '%s' "$styled" | sed 's/^"//; s/"$//')
|
|
336
|
+
# Only an EXPLICIT unstyled* verdict fails — a blank/garbled eval (browser
|
|
337
|
+
# hiccup) falls through to OK, since the DOM already mounted without error.
|
|
338
|
+
case "$styled" in
|
|
339
|
+
unstyled*)
|
|
340
|
+
echo "UNSTYLED"
|
|
341
|
+
echo "$styled"
|
|
342
|
+
echo "$out_png"
|
|
343
|
+
return
|
|
344
|
+
;;
|
|
345
|
+
esac
|
|
185
346
|
fi
|
|
186
347
|
|
|
187
348
|
echo "OK"
|
|
@@ -204,6 +365,13 @@ printf 'status\tfile\tscreenshot\tdetail\n' > "$TSV"
|
|
|
204
365
|
echo "|---|---|---|---|"
|
|
205
366
|
} > "$MD"
|
|
206
367
|
|
|
368
|
+
# Static import-graph lint before rendering (DDR-068) — only meaningful when
|
|
369
|
+
# system specimens are in scope (they own the shared _layout.css / _components.css
|
|
370
|
+
# chain). Violations are folded into the final exit code alongside render fails.
|
|
371
|
+
if [ "$INCLUDE_SYSTEM" = "1" ] && [ -d "$DESIGN_ROOT/system" ]; then
|
|
372
|
+
lint_specimen_imports
|
|
373
|
+
fi
|
|
374
|
+
|
|
207
375
|
FAILED=0
|
|
208
376
|
N=0
|
|
209
377
|
while IFS= read -r CANVAS; do
|
|
@@ -211,14 +379,20 @@ while IFS= read -r CANVAS; do
|
|
|
211
379
|
N=$((N + 1))
|
|
212
380
|
REL="${CANVAS#$DESIGN_ROOT/}"
|
|
213
381
|
REL_ENC=$(urlencode_path "$REL")
|
|
214
|
-
|
|
382
|
+
# Canvases render through the canvas shell, not the bare path. The bare
|
|
383
|
+
# `/<rel>` route 404s when the canvas-origin sandbox is on (default since
|
|
384
|
+
# phase-9.1) — only `/_canvas-shell.html?canvas=<rel>` mounts the canvas
|
|
385
|
+
# (works in both split-on and legacy same-origin modes).
|
|
386
|
+
URL="http://localhost:$PORT/_canvas-shell.html?canvas=$REL_ENC"
|
|
215
387
|
SLUG=$(bash "$SLUG_HELPER" "$REL" 2>/dev/null || printf '%s' "$REL" | tr '/ ' '__' | tr '[:upper:]' '[:lower:]')
|
|
216
388
|
OUT_PNG="$OUT_DIR/$SLUG.png"
|
|
389
|
+
# DS preview specimens get the computed-style gate; ui/ app canvases don't.
|
|
390
|
+
case "$REL" in system/*/preview/*) IS_SPECIMEN=1 ;; *) IS_SPECIMEN=0 ;; esac
|
|
217
391
|
|
|
218
392
|
printf ' [%d/%d] %s … ' "$N" "$COUNT" "$REL" >&2
|
|
219
393
|
|
|
220
394
|
if [ "$ENGINE" = "agent-browser" ]; then
|
|
221
|
-
RESULT=$(probe_agent_browser "$URL" "$OUT_PNG")
|
|
395
|
+
RESULT=$(probe_agent_browser "$URL" "$OUT_PNG" "$IS_SPECIMEN")
|
|
222
396
|
else
|
|
223
397
|
# Playwright fallback — coarser: just screenshot, accept any PNG > 2 KB as OK.
|
|
224
398
|
# See _screenshot-playwright.mjs for the underlying tool.
|
|
@@ -240,10 +414,11 @@ while IFS= read -r CANVAS; do
|
|
|
240
414
|
SHOT=$(printf '%s\n' "$RESULT" | sed -n '3p')
|
|
241
415
|
|
|
242
416
|
case "$STATUS" in
|
|
243
|
-
OK)
|
|
244
|
-
BLANK)
|
|
245
|
-
ERROR)
|
|
246
|
-
|
|
417
|
+
OK) SYM="✓"; echo "$SYM" >&2 ;;
|
|
418
|
+
BLANK) SYM="✗"; FAILED=$((FAILED + 1)); echo "$SYM blank ($DETAIL)" >&2 ;;
|
|
419
|
+
ERROR) SYM="⚠"; FAILED=$((FAILED + 1)); echo "$SYM error ($DETAIL)" >&2 ;;
|
|
420
|
+
UNSTYLED) SYM="✗"; FAILED=$((FAILED + 1)); echo "$SYM unstyled ($DETAIL)" >&2 ;;
|
|
421
|
+
*) SYM="?"; FAILED=$((FAILED + 1)); echo "? unknown ($STATUS)" >&2 ;;
|
|
247
422
|
esac
|
|
248
423
|
|
|
249
424
|
printf '%s\t%s\t%s\t%s\n' "$STATUS" "$REL" "$SHOT" "$DETAIL" >> "$TSV"
|
|
@@ -252,21 +427,31 @@ while IFS= read -r CANVAS; do
|
|
|
252
427
|
printf '%s\t%s\t%s\t%s\n' "$STATUS" "$REL" "$SHOT" "$DETAIL"
|
|
253
428
|
done <<< "$CANVASES"
|
|
254
429
|
|
|
430
|
+
TOTAL_FAIL=$((FAILED + LINT_FAILED))
|
|
255
431
|
{
|
|
256
432
|
echo
|
|
257
|
-
if [ $
|
|
258
|
-
echo "**Result:** ✓ all $COUNT canvases rendered."
|
|
433
|
+
if [ $TOTAL_FAIL -eq 0 ]; then
|
|
434
|
+
echo "**Result:** ✓ all $COUNT canvases rendered styled; import-graph lint clean."
|
|
259
435
|
else
|
|
260
|
-
echo "**Result:** ✗ $FAILED / $COUNT canvases failed."
|
|
436
|
+
echo "**Result:** ✗ $FAILED / $COUNT canvases failed render/style; $LINT_FAILED import-graph lint violation(s)."
|
|
261
437
|
fi
|
|
262
438
|
} >> "$MD"
|
|
263
439
|
|
|
264
440
|
echo "→ report: $MD" >&2
|
|
265
441
|
echo "→ tsv: $TSV" >&2
|
|
266
442
|
|
|
267
|
-
|
|
268
|
-
|
|
443
|
+
# Record this run as the baseline for the next --changed-only diff (Phase C / DDR-061).
|
|
444
|
+
if command -v git >/dev/null 2>&1 && git -C "$REPO" rev-parse HEAD >/dev/null 2>&1; then
|
|
445
|
+
mkdir -p "$DESIGN_ROOT/_history/_smoke"
|
|
446
|
+
printf '{"sha":"%s","ts":"%s","mode":"%s","count":%d,"failed":%d}' \
|
|
447
|
+
"$(git -C "$REPO" rev-parse HEAD 2>/dev/null)" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
448
|
+
"$([ "$CHANGED_ONLY" = "1" ] && echo changed-only || echo full)" "$COUNT" "$FAILED" \
|
|
449
|
+
> "$DESIGN_ROOT/_history/_smoke/.last-smoke.json"
|
|
450
|
+
fi
|
|
451
|
+
|
|
452
|
+
if [ $TOTAL_FAIL -gt 0 ]; then
|
|
453
|
+
echo "✗ smoke: $FAILED / $COUNT canvases failed render/style; $LINT_FAILED lint violation(s)" >&2
|
|
269
454
|
exit 3
|
|
270
455
|
fi
|
|
271
|
-
echo "✓ smoke: all $COUNT canvases rendered" >&2
|
|
456
|
+
echo "✓ smoke: all $COUNT canvases rendered styled; import-graph lint clean" >&2
|
|
272
457
|
exit 0
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file canvas-arrowheads.ts — Phase 24 arrow shaft + head geometry
|
|
3
|
+
* @scope plugins/design/dev-server/canvas-arrowheads.ts
|
|
4
|
+
* @purpose Single source of truth for arrow rendering. An ArrowStroke is
|
|
5
|
+
* reduced to an ordered list of `SvgPrimitive`s (the shaft, then
|
|
6
|
+
* the start head, then the end head). The serializer formats each
|
|
7
|
+
* primitive into the persisted `.annotations.svg` string; the live
|
|
8
|
+
* `StrokeNode` maps the SAME primitives to JSX — so the on-disk and
|
|
9
|
+
* on-canvas forms can never drift.
|
|
10
|
+
*
|
|
11
|
+
* Back-compat invariant (DDR-067): a default arrow (lineType
|
|
12
|
+
* 'straight', startHead 'none', endHead 'triangle', solid) MUST
|
|
13
|
+
* reduce to exactly `[<line>, <polyline fill=color>]` — the
|
|
14
|
+
* byte-identical Phase-5.1 form the canary fixtures freeze.
|
|
15
|
+
* `arrowHeadPoints` lives here (moved from annotations-layer, which
|
|
16
|
+
* re-exports it). This module owns the arrow style enums + a
|
|
17
|
+
* structural geometry input and imports NOTHING from
|
|
18
|
+
* annotations-layer — keeping the dependency one-way
|
|
19
|
+
* (annotations-layer → canvas-arrowheads) so there is no module
|
|
20
|
+
* cycle. (A `.ts` root in a re-export cycle with a react `.tsx`
|
|
21
|
+
* breaks @types/react's global `JSX` namespace project-wide under
|
|
22
|
+
* this tsconfig's `types: ["bun-types"]` — see DDR-067.)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Phase 24 — full FigJam arrowhead vocabulary, selectable per end. `none` =
|
|
27
|
+
* bare end; `line` = open chevron; `triangle` = the legacy filled head
|
|
28
|
+
* (default end); `triangle-outline` = unfilled triangle; `circle` / `diamond`
|
|
29
|
+
* = filled markers. annotations-layer re-exports this type.
|
|
30
|
+
*/
|
|
31
|
+
export type ArrowHead = 'none' | 'line' | 'triangle' | 'triangle-outline' | 'circle' | 'diamond';
|
|
32
|
+
/** The valid `ArrowHead` vocabulary as a runtime set — `svgToStrokes` clamps a
|
|
33
|
+
* parsed `data-*-head` against this so a poisoned/out-of-vocab value can't be
|
|
34
|
+
* cast through unchecked (Phase 24 security review, DDR-067). */
|
|
35
|
+
export const ARROW_HEADS: ReadonlySet<string> = new Set<ArrowHead>([
|
|
36
|
+
'none',
|
|
37
|
+
'line',
|
|
38
|
+
'triangle',
|
|
39
|
+
'triangle-outline',
|
|
40
|
+
'circle',
|
|
41
|
+
'diamond',
|
|
42
|
+
]);
|
|
43
|
+
/** Phase 24 — arrow shaft routing. `straight` is the default + legacy form. */
|
|
44
|
+
export type ArrowLineType = 'straight' | 'curved' | 'elbow';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Structural geometry input for {@link arrowPrimitives}. `ArrowStroke` (defined
|
|
48
|
+
* in annotations-layer) is assignable to this, so we depend on the shape, not
|
|
49
|
+
* the named type — no import back, no cycle.
|
|
50
|
+
*/
|
|
51
|
+
export interface ArrowGeom {
|
|
52
|
+
x1: number;
|
|
53
|
+
y1: number;
|
|
54
|
+
x2: number;
|
|
55
|
+
y2: number;
|
|
56
|
+
width: number;
|
|
57
|
+
color: string;
|
|
58
|
+
startHead?: ArrowHead;
|
|
59
|
+
endHead?: ArrowHead;
|
|
60
|
+
dashed?: boolean;
|
|
61
|
+
lineType?: ArrowLineType;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The filled-triangle head outline as an SVG `points` string. Three points —
|
|
66
|
+
* `wingA, tip, wingB` — drawn as a <polyline> (filled = solid triangle, the
|
|
67
|
+
* implicit close; unfilled = open chevron) or a <polygon> (closed outline).
|
|
68
|
+
* Parameterized by the tip + the point the shaft arrives FROM, exactly as in
|
|
69
|
+
* Phase 5.1, so the legacy serialization is byte-identical.
|
|
70
|
+
*/
|
|
71
|
+
export function arrowHeadPoints(
|
|
72
|
+
x1: number,
|
|
73
|
+
y1: number,
|
|
74
|
+
x2: number,
|
|
75
|
+
y2: number,
|
|
76
|
+
width: number
|
|
77
|
+
): string {
|
|
78
|
+
const angle = Math.atan2(y2 - y1, x2 - x1);
|
|
79
|
+
const len = 12 + width * 2;
|
|
80
|
+
const wing = Math.PI / 7;
|
|
81
|
+
const ax = x2 - Math.cos(angle - wing) * len;
|
|
82
|
+
const ay = y2 - Math.sin(angle - wing) * len;
|
|
83
|
+
const bx = x2 - Math.cos(angle + wing) * len;
|
|
84
|
+
const by = y2 - Math.sin(angle + wing) * len;
|
|
85
|
+
return `${ax},${ay} ${x2},${y2} ${bx},${by}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A small diamond head centered on the tip, one axis along the shaft. */
|
|
89
|
+
function diamondHeadPoints(
|
|
90
|
+
fromX: number,
|
|
91
|
+
fromY: number,
|
|
92
|
+
tipX: number,
|
|
93
|
+
tipY: number,
|
|
94
|
+
width: number
|
|
95
|
+
): string {
|
|
96
|
+
const angle = Math.atan2(tipY - fromY, tipX - fromX);
|
|
97
|
+
const half = 5 + width;
|
|
98
|
+
const ax = Math.cos(angle);
|
|
99
|
+
const ay = Math.sin(angle);
|
|
100
|
+
const px = -ay;
|
|
101
|
+
const py = ax;
|
|
102
|
+
const p = (dx: number, dy: number) => `${tipX + dx},${tipY + dy}`;
|
|
103
|
+
return `${p(ax * half, ay * half)} ${p(px * half, py * half)} ${p(-ax * half, -ay * half)} ${p(
|
|
104
|
+
-px * half,
|
|
105
|
+
-py * half
|
|
106
|
+
)}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Ordered SVG primitives — the shaft + heads. The renderer/serializer format these. */
|
|
110
|
+
export type SvgPrimitive =
|
|
111
|
+
| { el: 'line'; x1: number; y1: number; x2: number; y2: number; dash: boolean }
|
|
112
|
+
| { el: 'path'; d: string; dash: boolean }
|
|
113
|
+
| { el: 'polyline'; points: string; fill: string }
|
|
114
|
+
| { el: 'polygon'; points: string; fill: string }
|
|
115
|
+
| { el: 'circle'; cx: number; cy: number; r: number; fill: string };
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Shaft path for a non-straight arrow + the control point each head orients
|
|
119
|
+
* toward (its tangent source). Straight arrows never call this — they use a
|
|
120
|
+
* bare <line> so the legacy bytes are preserved.
|
|
121
|
+
*/
|
|
122
|
+
function shaftPath(
|
|
123
|
+
x1: number,
|
|
124
|
+
y1: number,
|
|
125
|
+
x2: number,
|
|
126
|
+
y2: number,
|
|
127
|
+
lineType: Exclude<ArrowLineType, 'straight'>
|
|
128
|
+
): { d: string; startFrom: [number, number]; endFrom: [number, number] } {
|
|
129
|
+
if (lineType === 'curved') {
|
|
130
|
+
const mx = (x1 + x2) / 2;
|
|
131
|
+
const my = (y1 + y2) / 2;
|
|
132
|
+
const dx = x2 - x1;
|
|
133
|
+
const dy = y2 - y1;
|
|
134
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
135
|
+
// Perpendicular bow — 22% of the chord, consistent side so curve is stable.
|
|
136
|
+
const off = len * 0.22;
|
|
137
|
+
const cx = mx + (-dy / len) * off;
|
|
138
|
+
const cy = my + (dx / len) * off;
|
|
139
|
+
return { d: `M${x1} ${y1} Q${cx} ${cy} ${x2} ${y2}`, startFrom: [cx, cy], endFrom: [cx, cy] };
|
|
140
|
+
}
|
|
141
|
+
// elbow — one orthogonal bend, along the dominant axis first.
|
|
142
|
+
const corner: [number, number] = Math.abs(x2 - x1) >= Math.abs(y2 - y1) ? [x2, y1] : [x1, y2];
|
|
143
|
+
return {
|
|
144
|
+
d: `M${x1} ${y1} L${corner[0]} ${corner[1]} L${x2} ${y2}`,
|
|
145
|
+
startFrom: corner,
|
|
146
|
+
endFrom: corner,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function headPrimitives(
|
|
151
|
+
head: ArrowHead,
|
|
152
|
+
tipX: number,
|
|
153
|
+
tipY: number,
|
|
154
|
+
fromX: number,
|
|
155
|
+
fromY: number,
|
|
156
|
+
width: number,
|
|
157
|
+
color: string
|
|
158
|
+
): SvgPrimitive[] {
|
|
159
|
+
switch (head) {
|
|
160
|
+
case 'none':
|
|
161
|
+
return [];
|
|
162
|
+
case 'triangle':
|
|
163
|
+
// Legacy filled head — byte-identical to Phase 5.1.
|
|
164
|
+
return [
|
|
165
|
+
{ el: 'polyline', points: arrowHeadPoints(fromX, fromY, tipX, tipY, width), fill: color },
|
|
166
|
+
];
|
|
167
|
+
case 'line':
|
|
168
|
+
// Open chevron — same 3 points, unfilled (no implicit close).
|
|
169
|
+
return [
|
|
170
|
+
{ el: 'polyline', points: arrowHeadPoints(fromX, fromY, tipX, tipY, width), fill: 'none' },
|
|
171
|
+
];
|
|
172
|
+
case 'triangle-outline':
|
|
173
|
+
// Closed triangle outline — polygon auto-closes the third edge.
|
|
174
|
+
return [
|
|
175
|
+
{ el: 'polygon', points: arrowHeadPoints(fromX, fromY, tipX, tipY, width), fill: 'none' },
|
|
176
|
+
];
|
|
177
|
+
case 'diamond':
|
|
178
|
+
return [
|
|
179
|
+
{ el: 'polygon', points: diamondHeadPoints(fromX, fromY, tipX, tipY, width), fill: color },
|
|
180
|
+
];
|
|
181
|
+
case 'circle':
|
|
182
|
+
return [{ el: 'circle', cx: tipX, cy: tipY, r: 4 + width, fill: color }];
|
|
183
|
+
default:
|
|
184
|
+
// Defensive: an out-of-vocabulary head (e.g. a poisoned hub-pushed value
|
|
185
|
+
// that slipped past the parse-clamp) renders NOTHING rather than throwing
|
|
186
|
+
// a TypeError mid-render — no per-stroke render DoS (Phase 24 security
|
|
187
|
+
// review, DDR-067). Parse-clamp + serialize-esc are the primary controls.
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reduce an arrow to its ordered SVG primitives: shaft, then start head, then
|
|
194
|
+
* end head (matching the legacy serialization order). Defaults: start 'none',
|
|
195
|
+
* end 'triangle', lineType 'straight', solid.
|
|
196
|
+
*/
|
|
197
|
+
export function arrowPrimitives(s: ArrowGeom): SvgPrimitive[] {
|
|
198
|
+
const startHead = s.startHead ?? 'none';
|
|
199
|
+
const endHead = s.endHead ?? 'triangle';
|
|
200
|
+
const dashed = s.dashed ?? false;
|
|
201
|
+
const lineType = s.lineType ?? 'straight';
|
|
202
|
+
const out: SvgPrimitive[] = [];
|
|
203
|
+
|
|
204
|
+
// Default tangent sources (straight): each head points away from the far end.
|
|
205
|
+
let startFrom: [number, number] = [s.x2, s.y2];
|
|
206
|
+
let endFrom: [number, number] = [s.x1, s.y1];
|
|
207
|
+
|
|
208
|
+
if (lineType === 'straight') {
|
|
209
|
+
out.push({ el: 'line', x1: s.x1, y1: s.y1, x2: s.x2, y2: s.y2, dash: dashed });
|
|
210
|
+
} else {
|
|
211
|
+
const shaft = shaftPath(s.x1, s.y1, s.x2, s.y2, lineType);
|
|
212
|
+
out.push({ el: 'path', d: shaft.d, dash: dashed });
|
|
213
|
+
startFrom = shaft.startFrom;
|
|
214
|
+
endFrom = shaft.endFrom;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
out.push(...headPrimitives(startHead, s.x1, s.y1, startFrom[0], startFrom[1], s.width, s.color));
|
|
218
|
+
out.push(...headPrimitives(endHead, s.x2, s.y2, endFrom[0], endFrom[1], s.width, s.color));
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
@@ -48,29 +48,14 @@ import {
|
|
|
48
48
|
import { MaybeToolProvider, useToolMode } from './use-tool-mode.tsx';
|
|
49
49
|
|
|
50
50
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
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
|
-
}
|
|
51
|
+
// Phase 24 — the comment-mode cursor (and every other tool cursor) is owned
|
|
52
|
+
// SOLELY by use-tool-mode.tsx, whose `* { cursor: <Kenney glyph> !important }`
|
|
53
|
+
// rule is injected by the ToolProvider this layer mounts (MaybeToolProvider) —
|
|
54
|
+
// so it covers bare DS specimens too. The old `[data-mc-host]…`/`body[…]`
|
|
55
|
+
// comment-cursor rule used to live here, but its higher specificity shadowed
|
|
56
|
+
// the unified Kenney cursor on UI canvases (comment-mount stamps
|
|
57
|
+
// `data-active-tool` on <body>, so `body[data-active-tool="comment"] *` matched
|
|
58
|
+
// there as well). Removed so the single source of truth wins. See DDR-067.
|
|
74
59
|
|
|
75
60
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
76
61
|
// CommentHost — owns the lite comment subsystem: the input router (comment-
|
|
@@ -116,7 +101,6 @@ function pickSpecimenEl(clientX: number, clientY: number): HTMLElement | null {
|
|
|
116
101
|
}
|
|
117
102
|
|
|
118
103
|
function CommentHost({ children, file }: { children: ReactNode; file: string | undefined }) {
|
|
119
|
-
ensureMountCursorStyles();
|
|
120
104
|
const { tool, setTool } = useToolMode();
|
|
121
105
|
const selSet = useSelectionSet();
|
|
122
106
|
const hostRef = useRef<HTMLDivElement | null>(null);
|