@zenuml/core 3.47.1 → 3.47.2

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.
Files changed (30) hide show
  1. package/.agents/skills/babysit-pr/SKILL.md +223 -0
  2. package/.agents/skills/babysit-pr/agents/openai.yaml +7 -0
  3. package/.agents/skills/dia-scoring/SKILL.md +139 -0
  4. package/.agents/skills/dia-scoring/agents/openai.yaml +7 -0
  5. package/.agents/skills/dia-scoring/references/selectors-and-keys.md +253 -0
  6. package/.agents/skills/land-pr/SKILL.md +120 -0
  7. package/.agents/skills/propagate-core-release/SKILL.md +205 -0
  8. package/.agents/skills/propagate-core-release/agents/openai.yaml +7 -0
  9. package/.agents/skills/propagate-core-release/references/downstreams.md +42 -0
  10. package/.agents/skills/ship-branch/SKILL.md +105 -0
  11. package/.agents/skills/submit-branch/SKILL.md +76 -0
  12. package/.agents/skills/validate-branch/SKILL.md +72 -0
  13. package/.claude/skills/emoji-eval/SKILL.md +187 -0
  14. package/.claude/skills/propagate-core-release/SKILL.md +81 -76
  15. package/.claude/skills/propagate-core-release/agents/openai.yaml +2 -2
  16. package/AGENTS.md +1 -1
  17. package/dist/stats.html +1 -1
  18. package/dist/zenuml.esm.mjs +16092 -15337
  19. package/dist/zenuml.js +540 -535
  20. package/docs/superpowers/plans/2026-03-30-emoji-support.md +1220 -0
  21. package/docs/superpowers/plans/2026-03-30-self-correcting-scoring.md +206 -0
  22. package/e2e/data/compare-cases.js +233 -0
  23. package/e2e/tools/compare-case.html +16 -2
  24. package/package.json +3 -3
  25. package/playwright.config.ts +1 -1
  26. package/scripts/analyze-compare-case/collect-data.mjs +139 -16
  27. package/scripts/analyze-compare-case/config.mjs +1 -1
  28. package/scripts/analyze-compare-case/report.mjs +3 -0
  29. package/scripts/analyze-compare-case/residual-scopes.mjs +23 -1
  30. package/scripts/analyze-compare-case/scoring.mjs +1 -0
@@ -0,0 +1,206 @@
1
+ # Self-Correcting Dia-Scoring Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add a gap-detection and self-correction validation loop to the dia-scoring skill so it automatically detects and fixes analyzer blind spots during normal scoring runs.
6
+
7
+ **Architecture:** The entire change is a new section in `SKILL.md` — behavioral instructions that tell the agent to cross-reference the diff panel's pixel clusters against the analyzer's coverage map, triage unaccounted clusters, and fix `collect-data.mjs` when the gap is a selector/extraction miss. No new scripts or tooling.
8
+
9
+ **Tech Stack:** Markdown (SKILL.md), Playwright browser_evaluate (runtime), collect-data.mjs (runtime fixes)
10
+
11
+ ---
12
+
13
+ ### Task 1: Add "Gap Detection and Self-Correction" section to SKILL.md
14
+
15
+ **Files:**
16
+ - Modify: `/Users/pengxiao/.claude/skills/dia-scoring/SKILL.md` (append new section after the existing "Known Analyzer Internals" section, before "Commands")
17
+
18
+ - [ ] **Step 1: Add the new section**
19
+
20
+ Open `/Users/pengxiao/.claude/skills/dia-scoring/SKILL.md` and insert the following section between the "Known Analyzer Internals" section (line 167) and the "Commands" section (line 175):
21
+
22
+ ```markdown
23
+ ## Gap Detection and Self-Correction
24
+
25
+ After running the analyzer and producing the JSON report, **automatically validate** that the report covers all visible diff clusters. Do not wait for the user to request calibration.
26
+
27
+ ### Step 1: Build a Coverage Map
28
+
29
+ Collect every fine-grained bounding box from the analyzer JSON:
30
+ - `html_box` / `svg_box` from labels, numbers, arrows
31
+ - `html_icon_box` / `svg_icon_box` from participant icons
32
+ - `label_box` from participant labels
33
+ - `stereotype_box`, `comment_box`, `divider_box`, occurrence boxes, etc.
34
+
35
+ Use the most specific boxes available (e.g., `icon_box` rather than the coarse `participant_box`) to avoid masking sub-element gaps.
36
+
37
+ ### Step 2: Find Unaccounted Diff Clusters
38
+
39
+ Scan the `#diff-panel canvas` for connected clusters of red (HTML-only) or blue (SVG-only) pixels. Filter noise (clusters < 20 pixels). A cluster is **covered** when:
40
+
41
+ - The cluster's centroid falls inside a reported element's bounding box, OR
42
+ - The overlap between the cluster and a reported element's box is >= 30% of the cluster's area
43
+
44
+ Clusters meeting neither condition are **unaccounted** and trigger investigation.
45
+
46
+ `colorDiff` (purple) pixels within a covered region are expected and do not trigger investigation.
47
+
48
+ ### Step 3: Verify Coordinate Mapping
49
+
50
+ Before inspecting the DOM at gap coordinates:
51
+
52
+ 1. Derive an initial canvas-to-page mapping from frame/canvas geometry (canvas natural size / frame CSS size).
53
+ 2. Probe a known anchor — pick a reported element with known page coordinates and verify the mapping lands on it via `document.elementFromPoint`.
54
+ 3. If the probe hits the wrong panel, empty space, or an unrelated element, recalibrate once using the probe result.
55
+ 4. If the mapping still fails after one recalibration, mark the cluster as `uncertain` and move on.
56
+
57
+ ### Step 4: Inspect DOM at Gap Coordinates
58
+
59
+ For each unaccounted cluster with a verified mapping:
60
+
61
+ 1. Use `document.elementFromPoint(x, y)` on both the HTML and SVG panels.
62
+ 2. Walk up to the semantic parent (participant, message, fragment) to understand the element's role.
63
+ 3. Classify: emoji icon, stereotype, arrow, label, or novel element.
64
+
65
+ ### Step 5: Triage
66
+
67
+ Classify each gap before acting:
68
+
69
+ - **`likely_analyzer_gap`** — Element exists on both sides, belongs to an existing scoring category (icons, labels, stereotypes, etc.), but the collection logic missed it. Proceed to fix.
70
+ - **`likely_renderer_residual`** — Element exists on only one side, or the difference is a genuine rendering discrepancy. Report in scoring output but do not modify the analyzer.
71
+ - **`uncertain`** — Cannot determine cause confidently. Report with DOM context and coordinates for manual review.
72
+
73
+ Only `likely_analyzer_gap` triggers a self-fix.
74
+
75
+ ### Step 6: Fix the Collection Logic
76
+
77
+ For `likely_analyzer_gap` clusters:
78
+
79
+ 1. Read the relevant collection function in `collect-data.mjs` (e.g., `collectHtmlParticipants` for participant sub-elements).
80
+ 2. Compare the function's selectors and extraction logic against the actual DOM element's tag, classes, and attributes.
81
+ 3. Identify why it wasn't matched.
82
+ 4. Fix the collection logic. This may include: adding selector patterns, adding fallback extraction paths, adjusting pairing logic, or modifying measurement paths. Keep changes targeted — no broad refactors.
83
+
84
+ This fixes the measurement tool, not the renderers.
85
+
86
+ ### Step 7: Re-run and Verify
87
+
88
+ 1. Re-run the analyzer on the target case. Confirm the previously-unaccounted cluster is now covered and semantically correct.
89
+ 2. Run 1-2 sibling cases with the same element family and confirm: populated data, no regression in previously-working sections.
90
+
91
+ ### Safety Limits
92
+
93
+ - Maximum **2 fix-and-rerun iterations** per scoring session.
94
+ - Only auto-fix `likely_analyzer_gap` that maps to an existing scoring category and collection function.
95
+ - Novel element types (no existing category): report as unresolved with element identity and coordinates.
96
+
97
+ ### Limitations
98
+
99
+ - **Invisible diffs**: If an element renders identically in both HTML and SVG (no diff pixels), this loop cannot detect that the analyzer doesn't cover it. The loop is reactive to visible differences only.
100
+ - **Novel categories**: The loop can detect and report novel element types but does not create new scoring categories autonomously.
101
+ ```
102
+
103
+ - [ ] **Step 2: Update the participant icons scope line**
104
+
105
+ In the same file, find line 74:
106
+
107
+ ```markdown
108
+ - participant icons (actor, database, ec2, lambda, azurefunction, sqs, sns, iam, boundary, control, entity)
109
+ ```
110
+
111
+ Replace with:
112
+
113
+ ```markdown
114
+ - participant icons (actor, database, ec2, lambda, azurefunction, sqs, sns, iam, boundary, control, entity, and emoji-based icons like 🌐, 🔒, 🗄️)
115
+ ```
116
+
117
+ This documents that emoji icons are in scope, which the self-correction loop discovered.
118
+
119
+ - [ ] **Step 3: Verify the edit is well-formed**
120
+
121
+ Read the modified SKILL.md and confirm:
122
+ - The new section appears between "Known Analyzer Internals" and "Commands"
123
+ - No existing sections were accidentally modified
124
+ - The markdown renders correctly (no broken formatting)
125
+
126
+ Run: `cat /Users/pengxiao/.claude/skills/dia-scoring/SKILL.md | head -250`
127
+ Expected: The new "Gap Detection and Self-Correction" section is visible, followed by the existing "Commands" section.
128
+
129
+ - [ ] **Step 4: Commit**
130
+
131
+ ```bash
132
+ cd /Users/pengxiao/workspaces/zenuml/mmd-zenuml-core
133
+ git add /Users/pengxiao/.claude/skills/dia-scoring/SKILL.md
134
+ git commit -m "feat(dia-scoring): add gap detection and self-correction validation loop"
135
+ ```
136
+
137
+ ---
138
+
139
+ ### Task 2: Validate the self-correction loop on the emoji-async-return case
140
+
141
+ This task verifies the new skill instructions work end-to-end by running a scoring session on the case that originally exposed the gap.
142
+
143
+ **Files:**
144
+ - No files created or modified — this is a validation task
145
+
146
+ - [ ] **Step 1: Run the analyzer on emoji-async-return**
147
+
148
+ ```bash
149
+ cd /Users/pengxiao/workspaces/zenuml/mmd-zenuml-core
150
+ node scripts/analyze-compare-case.mjs --case emoji-async-return --json 2>&1 | python3 -c "
151
+ import json, sys
152
+ data = json.load(sys.stdin)
153
+ icons = data.get('participant_icons', [])
154
+ print(f'participant_icons count: {len(icons)}')
155
+ for icon in icons:
156
+ print(f' {icon.get(\"name\", \"?\")} — presence html:{icon.get(\"presence\",{}).get(\"html\")} svg:{icon.get(\"presence\",{}).get(\"svg\")} status:{icon.get(\"status\")}')
157
+ "
158
+ ```
159
+
160
+ Expected: `participant_icons count: 0` (the analyzer doesn't detect emoji icons yet — this confirms the gap exists before the self-correction loop runs).
161
+
162
+ - [ ] **Step 2: Navigate to the compare-case page and read the diff panel**
163
+
164
+ Using Playwright:
165
+ 1. Navigate to `http://localhost:8080/e2e/tools/compare-case.html?case=emoji-async-return`
166
+ 2. Wait for `[native-diff-ext] Done!` in console
167
+ 3. Read the `#diff-panel canvas` pixel data via `browser_evaluate`
168
+ 4. Identify unaccounted clusters in the participant icon region
169
+
170
+ Expected: The diff panel shows HTML-only and SVG-only pixel clusters around the emoji icons (🌐, 🔒, 🗄️) that are not covered by any reported element in the analyzer output.
171
+
172
+ - [ ] **Step 3: Follow the self-correction loop**
173
+
174
+ Execute steps 1-7 of the new "Gap Detection and Self-Correction" section:
175
+ 1. Build coverage map from analyzer JSON
176
+ 2. Find unaccounted clusters (the icon regions)
177
+ 3. Verify coordinate mapping with a known anchor probe
178
+ 4. Inspect DOM at gap coordinates — find `span.mr-1.flex-shrink-0` (HTML) and emoji `tspan` (SVG)
179
+ 5. Triage as `likely_analyzer_gap` (element exists on both sides, belongs to participant icons category)
180
+ 6. Fix `collect-data.mjs` — add emoji icon detection to both `collectHtmlParticipants` and `collectSvgParticipants`
181
+ 7. Re-run analyzer, verify icons are now detected. Run 1-2 sibling emoji cases.
182
+
183
+ Expected: After the fix, `participant_icons count: 3` with all three emoji icons detected and scored.
184
+
185
+ - [ ] **Step 4: Commit the collect-data.mjs fix**
186
+
187
+ ```bash
188
+ cd /Users/pengxiao/workspaces/zenuml/mmd-zenuml-core
189
+ git add scripts/analyze-compare-case/collect-data.mjs
190
+ git commit -m "fix(analyzer): detect emoji-based participant icons in collect-data"
191
+ ```
192
+
193
+ ---
194
+
195
+ ### Task 3: Commit the design spec
196
+
197
+ **Files:**
198
+ - Stage: `docs/superpowers/specs/2026-03-30-self-correcting-scoring-design.md`
199
+
200
+ - [ ] **Step 1: Commit the spec**
201
+
202
+ ```bash
203
+ cd /Users/pengxiao/workspaces/zenuml/mmd-zenuml-core
204
+ git add docs/superpowers/specs/2026-03-30-self-correcting-scoring-design.md
205
+ git commit -m "docs: add self-correcting dia-scoring design spec"
206
+ ```
@@ -843,6 +843,39 @@ OrderService.handle() {
843
843
  B -> C: forward
844
844
  ==Done==`,
845
845
 
846
+ // --- Emoji ---
847
+ "emoji-participant": `[rocket] Production
848
+ Production.deploy()`,
849
+ "emoji-multi-participants": `[rocket] Production
850
+ [lock] Auth
851
+ [fire] Cache
852
+ Production->Auth: validate
853
+ Auth->Cache: lookup`,
854
+ "emoji-with-type": `@Database [fire] HotDB
855
+ @Actor [eyes] Reviewer
856
+ Reviewer->HotDB: query`,
857
+ "emoji-with-stereotype": `<<service>> [lock] Auth
858
+ <<gateway>> [rocket] API
859
+ API->Auth: authenticate`,
860
+ "emoji-no-emoji-baseline": `Production
861
+ Auth
862
+ Cache
863
+ Production->Auth: validate
864
+ Auth->Cache: lookup`,
865
+ "emoji-async-message": `A
866
+ B
867
+ A->B: [rocket] launching`,
868
+ "emoji-alt-condition": `A
869
+ B
870
+ A->B: [check] start
871
+ if(success) {
872
+ A->B: [rocket] proceed
873
+ }`,
874
+ "emoji-comment": `A
875
+ B
876
+ // [eyes] review this
877
+ A->B: process`,
878
+
846
879
  // --- Icons ---
847
880
  "icons": `@Actor User
848
881
  @Database DB
@@ -854,4 +887,204 @@ User.login() {
854
887
  MQ.enqueue()
855
888
  Topic.publish()
856
889
  }`,
890
+
891
+ // --- Emoji parity cases ---
892
+ "emoji-sync-call": `[rocket]A.method() {
893
+ [database]B.query()
894
+ }`,
895
+ "emoji-nested-calls": `[globe]API.handle() {
896
+ [lock]Auth.validate() {
897
+ [database]DB.lookup()
898
+ }
899
+ }`,
900
+ "emoji-async-return": `[globe]API->[lock]Auth: validate
901
+ Auth->[database]DB: lookup
902
+ DB-->Auth: [check] found
903
+ Auth-->API: [check] authorized`,
904
+ "emoji-with-fragment": `[rocket]Client->[lock]Server.request()
905
+ if(authorized) {
906
+ Server->[database]DB.query()
907
+ DB-->Server: [check] result
908
+ } else {
909
+ Server-->Client: [x] denied
910
+ }`,
911
+ "emoji-divider-case": `[rocket]A->[lock]B.start()
912
+ == [fire] Deploy Phase ==
913
+ B->[database]C.migrate()`,
914
+ "emoji-group-case": `group Backend {[database]DB [cache]Redis}
915
+ [globe]Gateway->DB.query()
916
+ Gateway->Redis.get()`,
917
+ "emoji-group-case-2groups": `group Backend {[database]DB [cache]Redis}
918
+ group Frontend {[globe]Gateway}
919
+ Gateway->DB.query()
920
+ Gateway->Redis.get()`,
921
+ "group-minimal": `group x {a}`,
922
+ "group-single-participant": `group Frontend {[globe]Gateway}
923
+ group Backend {[database]DB}
924
+ Gateway->DB.query()`,
925
+ "emoji-comment-styled": `// [eyes] monitoring
926
+ [rocket]A->[lock]B.deploy()
927
+ // [rocket, red] critical path
928
+ B->[database]C.write()`,
929
+ "emoji-colon-override": `[:red:] Alert
930
+ [rocket] Normal
931
+ Alert->Normal.notify()`,
932
+ "emoji-icon-combo": `@Actor [star] Admin
933
+ @Database [fire] HotDB
934
+ Admin->HotDB.query()`,
935
+ "emoji-long-names": `[rocket]ProductionServer->[lock]AuthService.validate()
936
+ AuthService->[database]UserDB.find()
937
+ UserDB-->AuthService: [check] found`,
938
+ "emoji-simple-async": `[rocket]A->[lock]B: hello
939
+ B-->A: [check] done`,
940
+ "emoji-self-call": `[gear]Processor.init() {
941
+ Processor.validate()
942
+ }`,
943
+ "emoji-title": `title [rocket] Deploy Pipeline
944
+ [lock]A->[database]B.save()`,
945
+ // emoji variants of existing patterns
946
+ "emoji-nested-sync-deep": `[rocket]A.methodA() {
947
+ [lock]B.methodB() {
948
+ [database]C.methodC() {
949
+ [fire]D.process()
950
+ }
951
+ }
952
+ }`,
953
+ "emoji-async-many": `[rocket]A [lock]B [database]C
954
+ A->B: [check] msg1
955
+ B->C: [fire] msg2
956
+ C->B: [check] result
957
+ B->A: [check] done`,
958
+ "emoji-if-else": `[rocket]Client->[lock]Server.request()
959
+ if(valid) {
960
+ Server->[database]DB.query()
961
+ } else {
962
+ Server-->Client: [x] denied
963
+ }`,
964
+ "emoji-tcf": `[globe]A.process() {
965
+ try {
966
+ [database]B.save()
967
+ } catch(e) {
968
+ [warning]C.handle()
969
+ } finally {
970
+ [gear]D.cleanup()
971
+ }
972
+ }`,
973
+ "emoji-loop": `[rocket]A->[lock]B.fetch()
974
+ loop(retries < 3) {
975
+ B->[database]C.query()
976
+ C-->B: [check] ok
977
+ }`,
978
+ "emoji-par": `[rocket]Orchestrator.run() {
979
+ par {
980
+ [database]DB.write()
981
+ [cache]Redis.flush()
982
+ }
983
+ }`,
984
+ "emoji-return-chain": `[globe]API->[lock]Auth.check() {
985
+ Auth->[database]DB.query() {
986
+ DB-->Auth: [check] found
987
+ }
988
+ Auth-->API: [check] valid
989
+ }`,
990
+ "emoji-creation-simple": `[rocket]A.init() {
991
+ new B()
992
+ }`,
993
+ "emoji-color": `[rocket] Prod #FF6600
994
+ [lock] Auth #0747A6
995
+ Prod->Auth.validate()`,
996
+ "emoji-stereotype-only": `<<service>> [lock] Auth
997
+ <<gateway>> [globe] API
998
+ API->Auth.check()`,
999
+ "emoji-method-name": `A.[rocket]deploy()
1000
+ A.[lock]validate()
1001
+ A->[database]B.[fire]save()`,
1002
+ "emoji-condition-label": `[rocket]Client->[lock]Server.request()
1003
+ if(authorized) {
1004
+ Server->[database]DB.query()
1005
+ DB-->Server: [check] result
1006
+ } else {
1007
+ Server-->Client: [x] denied
1008
+ }`,
1009
+ "emoji-in-conditions": `if([check] authorized) {
1010
+ A.proceed()
1011
+ } else if([warning] rate limited) {
1012
+ A.wait()
1013
+ } else {
1014
+ A.deny()
1015
+ }`,
1016
+ "emoji-tcf-labels": `A.process() {
1017
+ try {
1018
+ B.save()
1019
+ } catch(DatabaseError) {
1020
+ C.rollback()
1021
+ } finally {
1022
+ D.cleanup()
1023
+ }
1024
+ }`,
1025
+ "emoji-loop-condition": `while([rocket] deploying) {
1026
+ A->[database]B.check()
1027
+ B-->A: [check] status
1028
+ }`,
1029
+ "emoji-opt-critical": `[rocket]A->[lock]B.request()
1030
+ opt {
1031
+ B.[gear]process()
1032
+ }
1033
+ critical([warning] important) {
1034
+ B->[database]C.save()
1035
+ }`,
1036
+ "emoji-nested-mixed": `[globe]Client->[lock]Server.handle() {
1037
+ if([check] cached) {
1038
+ Server->[cache]Redis.[rocket]get()
1039
+ } else {
1040
+ Server->[database]DB.[fire]query() {
1041
+ try {
1042
+ DB.[gear]process()
1043
+ } catch(timeout) {
1044
+ DB-->Server: [warning] retry
1045
+ }
1046
+ }
1047
+ }
1048
+ }`,
1049
+ "emoji-all-features": `title [rocket] System Overview
1050
+ @Actor [star] Admin
1051
+ @Database [fire] DB
1052
+ <<service>> [lock] Auth
1053
+ [globe] API
1054
+
1055
+ // [eyes] authentication flow
1056
+ Admin->API.[key]login(credentials)
1057
+ API->Auth.[lock]validate(token) {
1058
+ if([check] valid) {
1059
+ Auth->DB.[fire]query(userId)
1060
+ DB-->Auth: [check] found
1061
+ } else {
1062
+ Auth-->API: [x] denied
1063
+ }
1064
+ }
1065
+ == [rocket] deploy phase ==
1066
+ API->[gear]Worker: [rocket] process`,
1067
+ "emoji-chained-calls": `[rocket]A.[lock]auth().[fire]process().[check]save()`,
1068
+ "emoji-assign-return": `[globe]API->[lock]Auth.check() {
1069
+ result = [database]DB.query()
1070
+ return [check] authorized
1071
+ }`,
1072
+ "emoji-multi-async": `[rocket]A->[lock]B: [fire] step 1
1073
+ B->[database]C: [gear] step 2
1074
+ C->[globe]D: [check] step 3
1075
+ D-->A: [check] all done`,
1076
+ "emoji-named-params": `[rocket]A.[lock]method(userId=123, name="John")
1077
+ [database]B.[fire]create(type="User", active=true)`,
1078
+ "emoji-self-sync": `[gear]selfSync() {
1079
+ [rocket]A.[lock]method() {
1080
+ [database]B.save()
1081
+ }
1082
+ }`,
1083
+ "emoji-fragments-return": `[rocket]A.[lock]method() {
1084
+ if([check] x) {
1085
+ return [check] success
1086
+ } else {
1087
+ return [x] failure
1088
+ }
1089
+ }`,
857
1090
  };
@@ -17,6 +17,9 @@
17
17
  .nav button { padding: 4px 12px; background: #7c3aed; border: none; border-radius: 4px; color: white; font-size: 12px; cursor: pointer; }
18
18
  .nav button:hover { background: #6d28d9; }
19
19
  .nav button:disabled { background: #64748b; cursor: wait; }
20
+ .nav-btn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; background: #334155; border: none; border-radius: 4px; color: white; font-size: 16px; cursor: pointer; text-decoration: none; line-height: 1; }
21
+ .nav-btn:hover { background: #475569; }
22
+ .nav-btn[style*="hidden"] { visibility: hidden; }
20
23
  .nav .match-badge { font-size: 11px; color: #94a3b8; }
21
24
  .container { display: flex; gap: 0; }
22
25
  .container.stacked { flex-direction: column; }
@@ -35,8 +38,8 @@
35
38
  <a href="/e2e/tools/compare.html">&larr; All cases</a>
36
39
  <h2 id="case-name"></h2>
37
40
  <div class="nav">
38
- <a id="prev-link" href="#">&larr; Prev</a>
39
- <a id="next-link" href="#">Next &rarr;</a>
41
+ <a id="prev-link" class="nav-btn" href="#" title="Previous case">&#8249;</a>
42
+ <a id="next-link" class="nav-btn" href="#" title="Next case">&#8250;</a>
40
43
  <span class="match-badge" id="match-badge"></span>
41
44
  </div>
42
45
  </div>
@@ -87,6 +90,15 @@
87
90
  nextLink.style.visibility = "hidden";
88
91
  }
89
92
 
93
+ // Keyboard navigation
94
+ document.addEventListener("keydown", (e) => {
95
+ if (e.key === "ArrowLeft" && prevLink.style.visibility !== "hidden") {
96
+ window.location.href = prevLink.href;
97
+ } else if (e.key === "ArrowRight" && nextLink.style.visibility !== "hidden") {
98
+ window.location.href = nextLink.href;
99
+ }
100
+ });
101
+
90
102
  // Expose DSL for the extension's batch mode to read
91
103
  window.__currentDSL = code || "";
92
104
 
@@ -296,7 +308,9 @@
296
308
  if (htmlFrame) {
297
309
  const frameRect2 = htmlFrame.getBoundingClientRect();
298
310
  // Hide SVG icons in the header area (top 35px) — checkbox, etc.
311
+ // Skip group outline overlays (data-group-overlay) which are functional rendering elements.
299
312
  htmlFrame.querySelectorAll("svg").forEach(svg => {
313
+ if (svg.hasAttribute("data-group-overlay")) return;
300
314
  const r = svg.getBoundingClientRect();
301
315
  if (r.width > 0 && (r.top - frameRect2.top) < 35) _hideEl(svg);
302
316
  });
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@zenuml/core",
3
- "version": "3.47.1",
3
+ "version": "3.47.2",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "url": "https://github.com/mermaid-js/zenuml-core"
8
8
  },
9
9
  "scripts": {
10
- "dev": "vite dev --port 8080 --host 0.0.0.0",
11
- "preview": "bun run --bun vite preview --port 8080 --host",
10
+ "dev": "vite dev --port 4000 --host 0.0.0.0",
11
+ "preview": "bun run --bun vite preview --port 4000 --host",
12
12
  "build:site": "bun run --bun vite build",
13
13
  "build:gh-pages": "bun run --bun vite build --mode gh-pages",
14
14
  "build": "bun run --bun vite build -c vite.config.lib.ts",
@@ -1,6 +1,6 @@
1
1
  import { defineConfig, devices } from "@playwright/test";
2
2
 
3
- const PORT = Number(process.env.PORT) || 8080;
3
+ const PORT = Number(process.env.PORT) || 4000;
4
4
  const BASE_URL = `http://127.0.0.1:${PORT}`;
5
5
 
6
6
  export default defineConfig({