@zenuml/core 3.47.8 → 3.48.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.
Files changed (204) hide show
  1. package/dist/cloud-icons-eHuugVSv.js.map +1 -0
  2. package/dist/zenuml.esm.mjs +2153 -2156
  3. package/dist/zenuml.esm.mjs.map +1 -0
  4. package/dist/zenuml.js +82 -82
  5. package/dist/zenuml.js.map +1 -0
  6. package/package.json +11 -1
  7. package/src/cli/zenuml.ts +1164 -0
  8. package/.agents/skills/babysit-pr/SKILL.md +0 -223
  9. package/.agents/skills/babysit-pr/agents/openai.yaml +0 -7
  10. package/.agents/skills/dia-scoring/SKILL.md +0 -139
  11. package/.agents/skills/dia-scoring/agents/openai.yaml +0 -7
  12. package/.agents/skills/dia-scoring/references/selectors-and-keys.md +0 -253
  13. package/.agents/skills/land-pr/SKILL.md +0 -120
  14. package/.agents/skills/propagate-core-release/SKILL.md +0 -205
  15. package/.agents/skills/propagate-core-release/agents/openai.yaml +0 -7
  16. package/.agents/skills/propagate-core-release/references/downstreams.md +0 -42
  17. package/.agents/skills/ship-branch/SKILL.md +0 -105
  18. package/.agents/skills/submit-branch/SKILL.md +0 -76
  19. package/.agents/skills/validate-branch/SKILL.md +0 -72
  20. package/.claude/commands/README.md +0 -162
  21. package/.claude/commands/analyze.md +0 -101
  22. package/.claude/commands/clarify.md +0 -158
  23. package/.claude/commands/code-review.md +0 -322
  24. package/.claude/commands/constitution.md +0 -73
  25. package/.claude/commands/create-docs.md +0 -309
  26. package/.claude/commands/full-context.md +0 -121
  27. package/.claude/commands/gemini-consult.md +0 -164
  28. package/.claude/commands/handoff.md +0 -146
  29. package/.claude/commands/implement.md +0 -56
  30. package/.claude/commands/plan.md +0 -43
  31. package/.claude/commands/refactor.md +0 -188
  32. package/.claude/commands/specify.md +0 -21
  33. package/.claude/commands/tasks.md +0 -62
  34. package/.claude/commands/update-docs.md +0 -314
  35. package/.claude/hooks/README.md +0 -270
  36. package/.claude/hooks/config/sensitive-patterns.json +0 -86
  37. package/.claude/hooks/gemini-context-injector.sh +0 -129
  38. package/.claude/hooks/mcp-security-scan.sh +0 -147
  39. package/.claude/hooks/notify.sh +0 -103
  40. package/.claude/hooks/setup/hook-setup.md +0 -96
  41. package/.claude/hooks/setup/settings.json.template +0 -63
  42. package/.claude/hooks/sounds/complete.wav +0 -0
  43. package/.claude/hooks/sounds/input-needed.wav +0 -0
  44. package/.claude/hooks/subagent-context-injector.sh +0 -65
  45. package/.claude/skills/babysit-pr/SKILL.md +0 -223
  46. package/.claude/skills/babysit-pr/agents/openai.yaml +0 -7
  47. package/.claude/skills/dia-scoring/SKILL.md +0 -139
  48. package/.claude/skills/dia-scoring/agents/openai.yaml +0 -7
  49. package/.claude/skills/dia-scoring/references/selectors-and-keys.md +0 -253
  50. package/.claude/skills/emoji-eval/SKILL.md +0 -187
  51. package/.claude/skills/land-pr/SKILL.md +0 -120
  52. package/.claude/skills/propagate-core-release/SKILL.md +0 -205
  53. package/.claude/skills/propagate-core-release/agents/openai.yaml +0 -7
  54. package/.claude/skills/propagate-core-release/references/downstreams.md +0 -42
  55. package/.claude/skills/ship-branch/SKILL.md +0 -105
  56. package/.claude/skills/submit-branch/SKILL.md +0 -76
  57. package/.claude/skills/validate-branch/SKILL.md +0 -72
  58. package/.claude/skills/zenuml-ux-research/SKILL.md +0 -183
  59. package/.claude/skills/zenuml-ux-research/references/assertion-catalog.md +0 -261
  60. package/.claude/skills/zenuml-ux-research/references/best-practices-overview.md +0 -56
  61. package/.claude/skills/zenuml-ux-research/references/report-template.md +0 -89
  62. package/.claude/skills/zenuml-ux-research/references/scenarios/edit-message-label.md +0 -37
  63. package/.claude/skills/zenuml-ux-research/references/scenarios/insert-message.md +0 -36
  64. package/.claude/skills/zenuml-ux-research/references/scenarios/insert-participant.md +0 -31
  65. package/.claude/skills/zenuml-ux-research/references/scenarios/rename-participant.md +0 -33
  66. package/.claude/skills/zenuml-ux-research/references/scenarios/undo-insert.md +0 -35
  67. package/.devcontainer/devcontainer.json +0 -21
  68. package/.dockerignore +0 -19
  69. package/.eslintrc.js +0 -39
  70. package/.git-blame-ignore-revs +0 -6
  71. package/.kiro/hooks/README.md +0 -38
  72. package/.kiro/hooks/session-sound-notification.js +0 -44
  73. package/.kiro/hooks/session-sound-notification.json +0 -23
  74. package/.mcp.json.example +0 -17
  75. package/.nvmrc +0 -1
  76. package/.prettierignore +0 -4
  77. package/.prettierrc +0 -1
  78. package/.specify/memory/constitution.md +0 -33
  79. package/.specify/scripts/bash/check-prerequisites.sh +0 -166
  80. package/.specify/scripts/bash/common.sh +0 -113
  81. package/.specify/scripts/bash/create-new-feature.sh +0 -97
  82. package/.specify/scripts/bash/setup-plan.sh +0 -60
  83. package/.specify/scripts/bash/update-agent-context.sh +0 -728
  84. package/.specify/templates/agent-file-template.md +0 -23
  85. package/.specify/templates/plan-template.md +0 -219
  86. package/.specify/templates/spec-template.md +0 -116
  87. package/.specify/templates/tasks-template.md +0 -127
  88. package/.storybook/main.ts +0 -25
  89. package/.storybook/preview.ts +0 -29
  90. package/.watchmanconfig +0 -3
  91. package/AGENTS.md +0 -26
  92. package/CLAUDE.md +0 -124
  93. package/DEPLOYMENT.md +0 -62
  94. package/Dockerfile +0 -36
  95. package/IMPLEMENTATION_PLAN.md +0 -163
  96. package/Integration/vanilla-js/index.html +0 -42
  97. package/MCP-ASSISTANT-RULES.md +0 -85
  98. package/README_CN.md +0 -15
  99. package/TUTORIAL.md +0 -116
  100. package/antlr/antlr-4.11.1-complete.jar +0 -0
  101. package/bun.lock +0 -1544
  102. package/bunfig.toml +0 -52
  103. package/docs/UNICODE_SUPPORT.md +0 -179
  104. package/docs/ai-context/deployment-infrastructure.md +0 -21
  105. package/docs/ai-context/docs-overview.md +0 -89
  106. package/docs/ai-context/handoff.md +0 -174
  107. package/docs/ai-context/project-structure.md +0 -160
  108. package/docs/ai-context/system-integration.md +0 -21
  109. package/docs/asciidoc/contributor.adoc +0 -54
  110. package/docs/asciidoc/create-my-own-theme.adoc +0 -149
  111. package/docs/asciidoc/images/creation-component.png +0 -0
  112. package/docs/asciidoc/images/creation-rtl.png +0 -0
  113. package/docs/asciidoc/images/message-arrow-rtl.png +0 -0
  114. package/docs/asciidoc/images/occurrence.png +0 -0
  115. package/docs/asciidoc/images/return-message-conflict.png +0 -0
  116. package/docs/asciidoc/images/shift-up-half-the-height.png +0 -0
  117. package/docs/asciidoc/images/three-layer-info-arch.png +0 -0
  118. package/docs/asciidoc/images/vertical-alignment.svg +0 -1
  119. package/docs/asciidoc/images/vertically-aligning.png +0 -0
  120. package/docs/asciidoc/index.adoc +0 -277
  121. package/docs/asciidoc/theme-debug-web-app.png +0 -0
  122. package/docs/asciidoc/tutorial.adoc +0 -22
  123. package/docs/asciidoc/user-css.png +0 -0
  124. package/docs/async-vs-sync-parser-rules.md +0 -81
  125. package/docs/divider-parser-allow-spaces.md +0 -38
  126. package/docs/highlighting-messages.md +0 -52
  127. package/docs/images/editor-sample.png +0 -0
  128. package/docs/inherited-vs-provided-from.md +0 -64
  129. package/docs/parser/Assignment.md +0 -8
  130. package/docs/parser/PARSER_IMPROVEMENTS_CC.md +0 -425
  131. package/docs/parser/grammar_review_gemini.md +0 -116
  132. package/docs/participants-function.md +0 -25
  133. package/docs/responsive-participant-margin.md +0 -52
  134. package/docs/starter.md +0 -9
  135. package/docs/superpowers/plans/2026-03-27-e2e-test-reorg.md +0 -698
  136. package/docs/superpowers/plans/2026-03-30-emoji-support.md +0 -1220
  137. package/docs/superpowers/plans/2026-03-30-self-correcting-scoring.md +0 -206
  138. package/docs/superpowers/plans/2026-04-15-keyboard-editing-on-diagram.md +0 -1992
  139. package/docs/superpowers/plans/2026-04-15-zenuml-ux-research-skill.md +0 -1452
  140. package/docs/ux-research/.gitkeep +0 -0
  141. package/docs/ux-research/2026-04-15-rename-participant.md +0 -156
  142. package/docs/ux-research/2026-04-18-insert-participant.md +0 -151
  143. package/docs/width-translate-and-offsets.md +0 -62
  144. package/docs/xss.md +0 -59
  145. package/e2e/data/compare-cases.js +0 -1090
  146. package/e2e/data/diff-algorithm.js +0 -199
  147. package/e2e/fixtures/create-message.html +0 -26
  148. package/e2e/fixtures/editable-label.html +0 -35
  149. package/e2e/fixtures/editable-span.html +0 -122
  150. package/e2e/fixtures/empty-diagram.html +0 -23
  151. package/e2e/fixtures/fixture.html +0 -31
  152. package/e2e/fixtures/insert-participant.html +0 -23
  153. package/e2e/fixtures/reorder-cross-fragment.html +0 -31
  154. package/e2e/fixtures/reorder-fragment.html +0 -29
  155. package/e2e/fixtures/reorder-message.html +0 -27
  156. package/e2e/fixtures/svg-test.html +0 -21
  157. package/e2e/fixtures/type-switch.html +0 -29
  158. package/e2e/tools/canonical-history.html +0 -908
  159. package/e2e/tools/compare-case.html +0 -371
  160. package/e2e/tools/compare.html +0 -35
  161. package/e2e/tools/native-diff-ext/background.js +0 -60
  162. package/e2e/tools/native-diff-ext/bridge.js +0 -26
  163. package/e2e/tools/native-diff-ext/content.js +0 -194
  164. package/e2e/tools/svg-preview.html +0 -56
  165. package/embed.html +0 -193
  166. package/eslint.config.mjs +0 -35
  167. package/firebase-debug.log +0 -108
  168. package/iframe-container-demo/diagram.html +0 -124
  169. package/iframe-container-demo/host.html +0 -817
  170. package/index.html +0 -771
  171. package/mermaid-zenuml-async-spa-auth.png +0 -0
  172. package/mermaid-zenuml-async-spa-auth.snapshot.md +0 -96
  173. package/newsletter/unicode-support-announcement.md +0 -134
  174. package/playground/creation.html +0 -53
  175. package/playground/message.html +0 -63
  176. package/playwright.config.ts +0 -40
  177. package/renderer.html +0 -366
  178. package/scripts/analyze-compare-case/collect-data.mjs +0 -1134
  179. package/scripts/analyze-compare-case/config.mjs +0 -102
  180. package/scripts/analyze-compare-case/geometry.mjs +0 -101
  181. package/scripts/analyze-compare-case/native-diff.mjs +0 -224
  182. package/scripts/analyze-compare-case/output.mjs +0 -74
  183. package/scripts/analyze-compare-case/panel-diff.mjs +0 -114
  184. package/scripts/analyze-compare-case/report.mjs +0 -162
  185. package/scripts/analyze-compare-case/residual-scopes.mjs +0 -347
  186. package/scripts/analyze-compare-case/scoring.mjs +0 -829
  187. package/scripts/analyze-compare-case.mjs +0 -149
  188. package/scripts/bump-version.js +0 -117
  189. package/scripts/snapshot-dual.js +0 -173
  190. package/scripts/update-snapshots.js +0 -70
  191. package/skills/dia-scoring/SKILL.md +0 -129
  192. package/skills/dia-scoring/agents/openai.yaml +0 -7
  193. package/skills/dia-scoring/references/selectors-and-keys.md +0 -253
  194. package/tailwind.config.js +0 -126
  195. package/test-compression.html +0 -274
  196. package/test-mermaid-zenuml.html +0 -57
  197. package/test-setup.ts +0 -124
  198. package/test-url-params.html +0 -192
  199. package/tsconfig.app.json +0 -31
  200. package/tsconfig.node.json +0 -24
  201. package/tsconfig.test.json +0 -9
  202. package/vite.config.lib.ts +0 -93
  203. package/vite.config.ts +0 -84
  204. package/wrangler.toml +0 -18
@@ -1,908 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <title>Canonical Score History</title>
6
- <style>
7
- @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=DM+Sans:wght@400;500;600;700&display=swap');
8
-
9
- * { box-sizing: border-box; margin: 0; }
10
- body {
11
- font-family: 'DM Sans', sans-serif;
12
- background: #0a0e1a;
13
- color: #c8d1e0;
14
- min-height: 100vh;
15
- }
16
-
17
- /* Header */
18
- .header {
19
- padding: 20px 28px 16px;
20
- border-bottom: 1px solid #1a2035;
21
- display: flex;
22
- align-items: baseline;
23
- gap: 16px;
24
- }
25
- .header h1 {
26
- font-family: 'JetBrains Mono', monospace;
27
- font-size: 16px;
28
- font-weight: 600;
29
- color: #e8ecf4;
30
- letter-spacing: -0.3px;
31
- }
32
- .header .summary {
33
- font-size: 12px;
34
- color: #5a6a85;
35
- font-family: 'JetBrains Mono', monospace;
36
- }
37
- .header .back-link {
38
- margin-left: auto;
39
- font-size: 12px;
40
- color: #4a7cff;
41
- text-decoration: none;
42
- opacity: 0.7;
43
- transition: opacity 0.15s;
44
- }
45
- .header .back-link:hover { opacity: 1; }
46
-
47
- /* Main layout */
48
- .layout {
49
- display: grid;
50
- grid-template-columns: 280px 1fr;
51
- height: calc(100vh - 57px);
52
- transition: grid-template-columns 0.2s ease;
53
- }
54
- .layout.collapsed {
55
- grid-template-columns: 0px 1fr;
56
- }
57
-
58
- /* Sidebar toggle */
59
- .sidebar-toggle {
60
- background: #1a2035;
61
- border: 1px solid #2a3555;
62
- color: #8899bb;
63
- font-size: 14px;
64
- cursor: pointer;
65
- padding: 4px 8px;
66
- border-radius: 4px;
67
- transition: color 0.15s;
68
- }
69
- .sidebar-toggle:hover { color: #e8ecf4; }
70
-
71
- /* Case selector panel */
72
- .case-panel {
73
- border-right: 1px solid #1a2035;
74
- display: flex;
75
- flex-direction: column;
76
- overflow: hidden;
77
- min-width: 0;
78
- }
79
- .layout.collapsed .case-panel {
80
- visibility: hidden;
81
- }
82
- .case-panel-header {
83
- padding: 12px 16px;
84
- border-bottom: 1px solid #1a2035;
85
- display: flex;
86
- align-items: center;
87
- justify-content: space-between;
88
- flex-shrink: 0;
89
- }
90
- .case-panel-title {
91
- font-size: 11px;
92
- font-weight: 600;
93
- text-transform: uppercase;
94
- letter-spacing: 0.8px;
95
- color: #5a6a85;
96
- }
97
- .case-panel-actions {
98
- display: flex;
99
- gap: 6px;
100
- }
101
- .case-panel-actions button {
102
- font-family: 'JetBrains Mono', monospace;
103
- font-size: 10px;
104
- padding: 3px 8px;
105
- border: 1px solid #1e2a42;
106
- border-radius: 4px;
107
- background: transparent;
108
- color: #5a6a85;
109
- cursor: pointer;
110
- transition: all 0.15s;
111
- }
112
- .case-panel-actions button:hover {
113
- border-color: #4a7cff;
114
- color: #4a7cff;
115
- }
116
- .case-list {
117
- flex: 1;
118
- overflow-y: auto;
119
- padding: 4px 0;
120
- }
121
- .case-list::-webkit-scrollbar { width: 4px; }
122
- .case-list::-webkit-scrollbar-track { background: transparent; }
123
- .case-list::-webkit-scrollbar-thumb { background: #1e2a42; border-radius: 2px; }
124
-
125
- .case-item {
126
- display: grid;
127
- grid-template-columns: 28px 1fr 52px;
128
- align-items: center;
129
- padding: 5px 12px 5px 8px;
130
- cursor: pointer;
131
- transition: background 0.1s;
132
- gap: 4px;
133
- }
134
- .case-item:hover { background: #0f1525; }
135
- .case-item.checked { background: #0d1428; }
136
-
137
- .case-item input[type="checkbox"] {
138
- appearance: none;
139
- width: 14px;
140
- height: 14px;
141
- border: 1.5px solid #2a3550;
142
- border-radius: 3px;
143
- cursor: pointer;
144
- position: relative;
145
- transition: all 0.15s;
146
- justify-self: center;
147
- }
148
- .case-item input[type="checkbox"]:checked {
149
- border-color: var(--case-color, #4a7cff);
150
- background: var(--case-color, #4a7cff);
151
- }
152
- .case-item input[type="checkbox"]:checked::after {
153
- content: '';
154
- position: absolute;
155
- left: 3px; top: 1px;
156
- width: 4px; height: 7px;
157
- border: solid #fff;
158
- border-width: 0 1.5px 1.5px 0;
159
- transform: rotate(45deg);
160
- }
161
-
162
- .case-item-name {
163
- font-size: 12px;
164
- color: #8a95aa;
165
- white-space: nowrap;
166
- overflow: hidden;
167
- text-overflow: ellipsis;
168
- transition: color 0.15s;
169
- }
170
- .case-item.checked .case-item-name { color: #c8d1e0; }
171
-
172
- .case-item-score {
173
- font-family: 'JetBrains Mono', monospace;
174
- font-size: 11px;
175
- text-align: right;
176
- font-variant-numeric: tabular-nums;
177
- }
178
-
179
- /* Color coding */
180
- .sc-100 { color: #34d399; }
181
- .sc-90 { color: #6ee7b7; }
182
- .sc-80 { color: #fbbf24; }
183
- .sc-70 { color: #f97316; }
184
- .sc-low { color: #ef4444; }
185
-
186
- /* Main content */
187
- .main {
188
- display: flex;
189
- flex-direction: column;
190
- overflow: hidden;
191
- }
192
-
193
- /* Chart area */
194
- .chart-area {
195
- padding: 20px 24px 12px;
196
- flex-shrink: 0;
197
- }
198
- .chart-info {
199
- display: flex;
200
- align-items: baseline;
201
- gap: 12px;
202
- margin-bottom: 12px;
203
- }
204
- .chart-title {
205
- font-size: 13px;
206
- font-weight: 600;
207
- color: #e8ecf4;
208
- }
209
- .chart-subtitle {
210
- font-family: 'JetBrains Mono', monospace;
211
- font-size: 11px;
212
- color: #5a6a85;
213
- }
214
- .chart-legend {
215
- display: flex;
216
- gap: 12px;
217
- flex-wrap: wrap;
218
- margin-left: auto;
219
- }
220
- .legend-item {
221
- display: flex;
222
- align-items: center;
223
- gap: 5px;
224
- font-size: 11px;
225
- color: #8a95aa;
226
- }
227
- .legend-dot {
228
- width: 8px;
229
- height: 3px;
230
- border-radius: 1px;
231
- }
232
- .chart-wrap {
233
- background: #0d1220;
234
- border: 1px solid #151d30;
235
- border-radius: 6px;
236
- padding: 16px 12px 8px;
237
- }
238
- .chart-wrap canvas {
239
- width: 100%;
240
- height: 240px;
241
- display: block;
242
- }
243
-
244
- /* History table */
245
- .table-area {
246
- flex: 1;
247
- overflow-y: auto;
248
- padding: 12px 24px 24px;
249
- }
250
- .table-area::-webkit-scrollbar { width: 5px; }
251
- .table-area::-webkit-scrollbar-track { background: transparent; }
252
- .table-area::-webkit-scrollbar-thumb { background: #1e2a42; border-radius: 3px; }
253
-
254
- table {
255
- width: 100%;
256
- table-layout: fixed;
257
- border-collapse: collapse;
258
- font-size: 12px;
259
- }
260
- th {
261
- text-align: left;
262
- padding: 8px 10px;
263
- border-bottom: 1px solid #1a2035;
264
- color: #5a6a85;
265
- font-weight: 500;
266
- font-size: 10px;
267
- text-transform: uppercase;
268
- letter-spacing: 0.5px;
269
- position: sticky;
270
- top: 0;
271
- background: #0a0e1a;
272
- z-index: 1;
273
- }
274
- td {
275
- padding: 6px 10px;
276
- border-bottom: 1px solid #0f1525;
277
- font-family: 'JetBrains Mono', monospace;
278
- font-size: 11px;
279
- }
280
- tr:hover td { background: #0d1220; }
281
- .ts { color: #4a5568; font-size: 11px; }
282
- .d-pos { color: #34d399; }
283
- .d-neg { color: #ef4444; }
284
- .d-zero { color: #2a3550; }
285
-
286
- .expand-btn {
287
- cursor: pointer;
288
- color: #4a7cff;
289
- border: none;
290
- background: none;
291
- font-family: 'JetBrains Mono', monospace;
292
- font-size: 11px;
293
- padding: 2px 6px;
294
- opacity: 0.6;
295
- transition: opacity 0.15s;
296
- }
297
- .expand-btn:hover { opacity: 1; }
298
- .detail-row { display: none; }
299
- .detail-row.visible { display: table-row; }
300
- .detail-row td { padding: 10px; }
301
- .case-grid {
302
- display: grid;
303
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
304
- gap: 3px 16px;
305
- font-size: 11px;
306
- }
307
- .case-grid-item {
308
- display: flex;
309
- justify-content: space-between;
310
- padding: 2px 0;
311
- }
312
- .case-grid-name { color: #5a6a85; }
313
-
314
- /* Case table header dot */
315
- .th-case-dot {
316
- display: inline-block;
317
- width: 6px;
318
- height: 6px;
319
- border-radius: 50%;
320
- margin-right: 4px;
321
- vertical-align: middle;
322
- }
323
-
324
- .no-data {
325
- text-align: center;
326
- padding: 60px 24px;
327
- color: #2a3550;
328
- font-size: 13px;
329
- }
330
-
331
- /* Selection indicator */
332
- .selection-count {
333
- font-family: 'JetBrains Mono', monospace;
334
- font-size: 10px;
335
- color: #4a7cff;
336
- background: #0d1428;
337
- border: 1px solid #1a2540;
338
- padding: 2px 7px;
339
- border-radius: 3px;
340
- }
341
- </style>
342
- </head>
343
- <body>
344
- <div class="header">
345
- <button class="sidebar-toggle" id="sidebar-toggle" title="Toggle sidebar">&#9776;</button>
346
- <h1>canonical scores</h1>
347
- <span class="summary" id="summary"></span>
348
- <a href="/e2e/tools/compare.html" class="back-link">compare cases &rarr;</a>
349
- </div>
350
-
351
- <div class="layout">
352
- <!-- Case selector -->
353
- <div class="case-panel">
354
- <div class="case-panel-header">
355
- <span class="case-panel-title">Cases</span>
356
- <div class="case-panel-actions">
357
- <button id="btn-clear">clear</button>
358
- <button id="btn-all">all</button>
359
- </div>
360
- </div>
361
- <div class="case-list" id="case-list"></div>
362
- </div>
363
-
364
- <!-- Main area -->
365
- <div class="main">
366
- <div class="chart-area">
367
- <div class="chart-info">
368
- <span class="chart-title" id="chart-title">Average Score Trend</span>
369
- <span class="chart-subtitle" id="chart-subtitle"></span>
370
- <div class="chart-legend" id="chart-legend"></div>
371
- <button id="metric-toggle" style="
372
- background: #1a2035; border: 1px solid #2a3555; border-radius: 4px;
373
- color: #8a95aa; font-family: 'JetBrains Mono', monospace; font-size: 11px;
374
- padding: 3px 10px; cursor: pointer; margin-left: 8px;
375
- transition: all 0.15s;
376
- " onmouseover="this.style.borderColor='#4a7cff';this.style.color='#c8d1e0'"
377
- onmouseout="this.style.borderColor='#2a3555';this.style.color='#8a95aa'"
378
- >pos avg</button>
379
- </div>
380
- <div class="chart-wrap">
381
- <canvas id="main-chart" height="240"></canvas>
382
- </div>
383
- </div>
384
-
385
- <div class="table-area" id="table-area">
386
- <div class="no-data" id="no-data" style="display:none">No runs recorded yet.</div>
387
- </div>
388
- </div>
389
- </div>
390
-
391
- <script>
392
- // --- Sidebar toggle ---
393
- document.getElementById('sidebar-toggle').addEventListener('click', () => {
394
- document.querySelector('.layout').classList.toggle('collapsed');
395
- });
396
-
397
- // --- Chart metric toggle ---
398
- let chartMetric = 'pos'; // 'pixel' or 'pos'
399
- function getRunAvg(run) {
400
- return chartMetric === 'pos' ? runAveragePos(run) : run.average;
401
- }
402
- document.addEventListener('DOMContentLoaded', () => {
403
- const btn = document.getElementById('metric-toggle');
404
- const updateBtn = () => {
405
- btn.textContent = chartMetric === 'pos' ? 'show pixel avg' : 'show pos avg';
406
- };
407
- updateBtn();
408
- btn.addEventListener('click', () => {
409
- chartMetric = chartMetric === 'pos' ? 'pixel' : 'pos';
410
- updateBtn();
411
- redrawChart();
412
- });
413
- });
414
-
415
- // --- Color palette for case lines ---
416
- const PALETTE = [
417
- '#4a7cff','#34d399','#f97316','#a78bfa','#f472b6',
418
- '#22d3ee','#facc15','#fb7185','#38bdf8','#a3e635',
419
- '#e879f9','#2dd4bf','#f87171','#818cf8','#fbbf24',
420
- '#67e8f9','#c084fc','#4ade80','#fb923c','#e2e8f0',
421
- ];
422
-
423
- function scoreClass(s) {
424
- if (s >= 100) return 'sc-100';
425
- if (s >= 90) return 'sc-90';
426
- if (s >= 80) return 'sc-80';
427
- if (s >= 70) return 'sc-70';
428
- return 'sc-low';
429
- }
430
-
431
- function deltaHtml(d) {
432
- if (d === null) return '<span class="d-zero">&mdash;</span>';
433
- const sign = d > 0 ? '+' : '';
434
- const cls = d > 0 ? 'd-pos' : d < 0 ? 'd-neg' : 'd-zero';
435
- return `<span class="${cls}">${sign}${d.toFixed(1)}</span>`;
436
- }
437
-
438
- function fmtDate(iso) {
439
- const d = new Date(iso);
440
- return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) +
441
- ' ' + d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
442
- }
443
-
444
- function caseScore(entry) {
445
- return typeof entry === 'object' ? entry.score : entry;
446
- }
447
-
448
- function casePosScore(entry) {
449
- if (typeof entry === 'object' && typeof entry.posScore === 'number') return entry.posScore;
450
- return caseScore(entry);
451
- }
452
-
453
- function runAveragePos(run) {
454
- if (typeof run.averagePos === 'number') return run.averagePos;
455
- const scores = Object.values(run.cases || {}).map(casePosScore);
456
- return scores.length
457
- ? parseFloat((scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1))
458
- : (typeof run.average === 'number' ? run.average : 0);
459
- }
460
-
461
- async function loadHistory() {
462
- if (typeof window.__getCanonicalHistory === 'function') {
463
- return await window.__getCanonicalHistory();
464
- }
465
- return new Promise((resolve, reject) => {
466
- const req = indexedDB.open('canonical-history', 1);
467
- req.onupgradeneeded = () => req.result.createObjectStore('runs', { keyPath: 'timestamp' });
468
- req.onsuccess = () => {
469
- const db = req.result;
470
- const tx = db.transaction('runs', 'readonly');
471
- const store = tx.objectStore('runs');
472
- const getAll = store.getAll();
473
- getAll.onsuccess = () => { db.close(); resolve(getAll.result); };
474
- getAll.onerror = () => { db.close(); reject(getAll.error); };
475
- };
476
- req.onerror = () => reject(req.error);
477
- });
478
- }
479
-
480
- // --- Global state ---
481
- let allRuns = []; // oldest first
482
- let allCaseData = []; // { name, entries, scores, latest, delta, min, max, color }
483
- let selectedCases = new Set();
484
-
485
- // --- Build case data ---
486
- function buildCaseData(runs) {
487
- const sorted = [...runs].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
488
- const names = new Set();
489
- sorted.forEach(r => Object.keys(r.cases).forEach(k => names.add(k)));
490
-
491
- const data = [];
492
- let colorIdx = 0;
493
- for (const name of names) {
494
- const entries = [];
495
- sorted.forEach(r => {
496
- const d = r.cases[name];
497
- if (d != null) entries.push({ timestamp: r.timestamp, score: caseScore(d), posScore: casePosScore(d) });
498
- });
499
- const scores = entries.map(e => e.score);
500
- const latest = scores[scores.length - 1];
501
- const prev = scores.length >= 2 ? scores[scores.length - 2] : null;
502
- data.push({
503
- name, entries, scores, latest,
504
- delta: prev !== null ? latest - prev : null,
505
- min: Math.min(...scores),
506
- max: Math.max(...scores),
507
- color: PALETTE[colorIdx % PALETTE.length],
508
- });
509
- colorIdx++;
510
- }
511
- data.sort((a, b) => b.latest - a.latest);
512
- return data;
513
- }
514
-
515
- // --- Render case selector ---
516
- function renderCaseList() {
517
- const container = document.getElementById('case-list');
518
- container.innerHTML = '';
519
-
520
- allCaseData.forEach((cd, idx) => {
521
- const div = document.createElement('div');
522
- div.className = 'case-item';
523
- div.innerHTML = `
524
- <input type="checkbox" id="cb-${idx}" style="--case-color:${cd.color}">
525
- <span class="case-item-name" title="${cd.name}">${cd.name}</span>
526
- <span class="case-item-score ${scoreClass(cd.latest)}">${cd.latest.toFixed(1)}%</span>
527
- `;
528
-
529
- const cb = div.querySelector('input');
530
- div.addEventListener('click', (e) => {
531
- if (e.target === cb) return;
532
- cb.checked = !cb.checked;
533
- cb.dispatchEvent(new Event('change'));
534
- });
535
- cb.addEventListener('change', () => {
536
- if (cb.checked) {
537
- selectedCases.add(cd.name);
538
- div.classList.add('checked');
539
- } else {
540
- selectedCases.delete(cd.name);
541
- div.classList.remove('checked');
542
- }
543
- onSelectionChanged();
544
- });
545
-
546
- container.appendChild(div);
547
- });
548
- }
549
-
550
- // --- Chart drawing ---
551
- function redrawChart() {
552
- const canvas = document.getElementById('main-chart');
553
- const ctx = canvas.getContext('2d');
554
- const dpr = window.devicePixelRatio || 1;
555
- const rect = canvas.getBoundingClientRect();
556
- canvas.width = rect.width * dpr;
557
- canvas.height = rect.height * dpr;
558
- ctx.scale(dpr, dpr);
559
-
560
- const w = rect.width, h = rect.height;
561
- const pad = { top: 24, right: 16, bottom: 28, left: 42 };
562
- const plotW = w - pad.left - pad.right;
563
- const plotH = h - pad.top - pad.bottom;
564
-
565
- const titleEl = document.getElementById('chart-title');
566
- const subtitleEl = document.getElementById('chart-subtitle');
567
- const legendEl = document.getElementById('chart-legend');
568
-
569
- if (selectedCases.size === 0) {
570
- // --- Aggregate mode: avg(all) per run ---
571
- const avgs = allRuns.map(r => getRunAvg(r));
572
- const metricLabel = chartMetric === 'pos' ? 'Pos Avg' : 'Pixel Avg';
573
-
574
- titleEl.textContent = metricLabel + ' Score Trend (canonical)';
575
- subtitleEl.textContent = allRuns.length + ' runs';
576
- legendEl.innerHTML = '';
577
-
578
- if (avgs.length === 0) return;
579
-
580
- const minY = Math.max(0, Math.floor((Math.min(...avgs) - 5) / 5) * 5);
581
- const maxY = 100;
582
- const range = maxY - minY || 1;
583
-
584
- drawGrid(ctx, w, h, pad, plotW, plotH, minY, maxY, range);
585
- drawXLabels(ctx, allRuns.map(r => r.timestamp), w, h, pad, plotW);
586
-
587
- // Area fill
588
- const grad = ctx.createLinearGradient(0, pad.top, 0, pad.top + plotH);
589
- grad.addColorStop(0, 'rgba(74,124,255,0.12)');
590
- grad.addColorStop(1, 'rgba(74,124,255,0)');
591
- ctx.fillStyle = grad;
592
- ctx.beginPath();
593
- avgs.forEach((v, i) => {
594
- const px = pad.left + (i / Math.max(1, avgs.length - 1)) * plotW;
595
- const py = pad.top + plotH - ((v - minY) / range) * plotH;
596
- i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
597
- });
598
- ctx.lineTo(pad.left + plotW, pad.top + plotH);
599
- ctx.lineTo(pad.left, pad.top + plotH);
600
- ctx.closePath();
601
- ctx.fill();
602
-
603
- // Line
604
- ctx.strokeStyle = '#4a7cff';
605
- ctx.lineWidth = 2;
606
- ctx.beginPath();
607
- avgs.forEach((v, i) => {
608
- const px = pad.left + (i / Math.max(1, avgs.length - 1)) * plotW;
609
- const py = pad.top + plotH - ((v - minY) / range) * plotH;
610
- i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
611
- });
612
- ctx.stroke();
613
-
614
- // Value labels (sparse) + run labels for labeled runs
615
- const labelStep = Math.max(1, Math.floor(avgs.length / 10));
616
- ctx.font = '500 10px JetBrains Mono, monospace';
617
- ctx.textAlign = 'center';
618
- avgs.forEach((v, i) => {
619
- const px = pad.left + (i / Math.max(1, avgs.length - 1)) * plotW;
620
- const py = pad.top + plotH - ((v - minY) / range) * plotH;
621
- const run = allRuns[i];
622
- // Always show dot + value for labeled runs or sparse interval
623
- const showValue = (i % labelStep === 0) || i === avgs.length - 1 || run.label;
624
- if (showValue) {
625
- ctx.fillStyle = run.label ? '#ff9944' : '#4a7cff';
626
- ctx.beginPath();
627
- ctx.arc(px, py, run.label ? 4 : 2.5, 0, Math.PI * 2);
628
- ctx.fill();
629
- ctx.fillStyle = '#8a95aa';
630
- ctx.fillText(v.toFixed(1), px, py - 8);
631
- }
632
- });
633
- } else {
634
- // --- Per-case mode ---
635
- const selected = allCaseData.filter(cd => selectedCases.has(cd.name));
636
- titleEl.textContent = selected.length === 1 ? selected[0].name : selected.length + ' cases selected';
637
- subtitleEl.textContent = '';
638
-
639
- // Build legend
640
- legendEl.innerHTML = selected.map(cd =>
641
- `<span class="legend-item"><span class="legend-dot" style="background:${cd.color}"></span>${cd.name}</span>`
642
- ).join('');
643
-
644
- // Compute Y range across all selected
645
- let allScores = [];
646
- selected.forEach(cd => allScores.push(...cd.scores));
647
- const minY = Math.max(0, Math.floor((Math.min(...allScores) - 5) / 5) * 5);
648
- const maxY = 100;
649
- const range = maxY - minY || 1;
650
-
651
- // Use the run timestamps as X axis (all runs, oldest first)
652
- const timestamps = allRuns.map(r => r.timestamp);
653
-
654
- drawGrid(ctx, w, h, pad, plotW, plotH, minY, maxY, range);
655
- drawXLabels(ctx, timestamps, w, h, pad, plotW);
656
-
657
- // Draw each case line
658
- selected.forEach(cd => {
659
- // Map entries to run indices
660
- const scoreMap = {};
661
- cd.entries.forEach(e => { scoreMap[e.timestamp] = e.score; });
662
-
663
- ctx.strokeStyle = cd.color;
664
- ctx.lineWidth = 1.8;
665
- ctx.beginPath();
666
- let started = false;
667
- timestamps.forEach((ts, i) => {
668
- if (scoreMap[ts] == null) return;
669
- const px = pad.left + (i / Math.max(1, timestamps.length - 1)) * plotW;
670
- const py = pad.top + plotH - ((scoreMap[ts] - minY) / range) * plotH;
671
- if (!started) { ctx.moveTo(px, py); started = true; }
672
- else ctx.lineTo(px, py);
673
- });
674
- ctx.stroke();
675
-
676
- // Latest dot
677
- const lastEntry = cd.entries[cd.entries.length - 1];
678
- const lastIdx = timestamps.indexOf(lastEntry.timestamp);
679
- if (lastIdx >= 0) {
680
- const px = pad.left + (lastIdx / Math.max(1, timestamps.length - 1)) * plotW;
681
- const py = pad.top + plotH - ((lastEntry.score - minY) / range) * plotH;
682
- ctx.fillStyle = cd.color;
683
- ctx.beginPath();
684
- ctx.arc(px, py, 3, 0, Math.PI * 2);
685
- ctx.fill();
686
- ctx.font = '500 10px JetBrains Mono, monospace';
687
- ctx.textAlign = 'center';
688
- ctx.fillText(lastEntry.score.toFixed(1), px, py - 8);
689
- }
690
- });
691
- }
692
- }
693
-
694
- function drawGrid(ctx, w, h, pad, plotW, plotH, minY, maxY, range) {
695
- ctx.strokeStyle = '#141b2d';
696
- ctx.lineWidth = 0.5;
697
- ctx.font = '10px JetBrains Mono, monospace';
698
- ctx.fillStyle = '#3a4560';
699
- ctx.textAlign = 'right';
700
- for (let y = minY; y <= maxY; y += 10) {
701
- const py = pad.top + plotH - ((y - minY) / range) * plotH;
702
- ctx.beginPath();
703
- ctx.moveTo(pad.left, py);
704
- ctx.lineTo(w - pad.right, py);
705
- ctx.stroke();
706
- ctx.fillText(y + '%', pad.left - 6, py + 3);
707
- }
708
- }
709
-
710
- function drawXLabels(ctx, timestamps, w, h, pad, plotW) {
711
- ctx.textAlign = 'center';
712
- ctx.fillStyle = '#3a4560';
713
- ctx.font = '10px JetBrains Mono, monospace';
714
- const step = Math.max(1, Math.floor(timestamps.length / 8));
715
- timestamps.forEach((ts, i) => {
716
- if (i % step !== 0 && i !== timestamps.length - 1) return;
717
- const px = pad.left + (i / Math.max(1, timestamps.length - 1)) * plotW;
718
- const d = new Date(ts);
719
- ctx.fillText(d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), px, h - 4);
720
- });
721
- }
722
-
723
- // --- Table rendering (reactive) ---
724
- function renderTable() {
725
- const area = document.getElementById('table-area');
726
- const noData = document.getElementById('no-data');
727
-
728
- if (allRuns.length === 0) {
729
- noData.style.display = '';
730
- return;
731
- }
732
- noData.style.display = 'none';
733
-
734
- if (selectedCases.size === 0) {
735
- renderAggregateTable(area);
736
- } else {
737
- renderCaseTable(area);
738
- }
739
- }
740
-
741
- function renderAggregateTable(area) {
742
- const newestFirst = [...allRuns].reverse();
743
- const table = document.createElement('table');
744
- table.innerHTML = `<thead><tr>
745
- <th style="width:28px"></th>
746
- <th>Date</th><th style="width:40%">Label</th><th>Avg</th><th>Pos Avg</th><th>Delta</th>
747
- <th>Cases</th><th>Time</th><th style="width:80px">Best</th><th style="width:80px">Worst</th>
748
- </tr></thead>`;
749
- const tbody = document.createElement('tbody');
750
-
751
- newestFirst.forEach((run, idx) => {
752
- const prev = idx < newestFirst.length - 1 ? newestFirst[idx + 1] : null;
753
- const avg = run.average;
754
- const avgPos = runAveragePos(run);
755
- const prevAvgPos = prev ? runAveragePos(prev) : null;
756
- const delta = prevAvgPos !== null ? avgPos - prevAvgPos : null;
757
- const scores = Object.entries(run.cases).map(([n, d]) => ({
758
- name: n, score: caseScore(d), posScore: casePosScore(d),
759
- })).sort((a, b) => b.posScore - a.posScore);
760
- const best = scores[0], worst = scores[scores.length - 1];
761
-
762
- const tr = document.createElement('tr');
763
- tr.innerHTML = `
764
- <td><button class="expand-btn" data-idx="${idx}">+</button></td>
765
- <td class="ts">${fmtDate(run.timestamp)}</td>
766
- <td style="color:#8b9cc0;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${run.label || ''}">${run.label || ''}</td>
767
- <td class="${scoreClass(avg)}" style="font-weight:bold">${avg.toFixed(1)}%</td>
768
- <td class="${scoreClass(avgPos)}" style="font-weight:bold">${avgPos.toFixed(1)}%</td>
769
- <td>${deltaHtml(delta)}</td>
770
- <td style="color:#5a6a85">${run.caseCount}</td>
771
- <td class="ts">${run.elapsed || '—'}s</td>
772
- <td class="${scoreClass(best.posScore)}" style="font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${best.name} ${best.posScore.toFixed(1)}%">${best.name} ${best.posScore.toFixed(1)}%</td>
773
- <td class="${scoreClass(worst.posScore)}" style="font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${worst.name} ${worst.posScore.toFixed(1)}%">${worst.name} ${worst.posScore.toFixed(1)}%</td>
774
- `;
775
- tbody.appendChild(tr);
776
-
777
- const detailTr = document.createElement('tr');
778
- detailTr.className = 'detail-row';
779
- detailTr.id = `detail-${idx}`;
780
- detailTr.innerHTML = `<td colspan="10"><div class="case-grid">${
781
- scores.map(s => `<div class="case-grid-item"><span class="case-grid-name">${s.name}</span><span class="${scoreClass(s.score)}">${s.score}% / ${s.posScore.toFixed(1)}%</span></div>`).join('')
782
- }</div></td>`;
783
- tbody.appendChild(detailTr);
784
- });
785
-
786
- tbody.addEventListener('click', (e) => {
787
- const btn = e.target.closest('.expand-btn');
788
- if (!btn) return;
789
- const detail = document.getElementById(`detail-${btn.dataset.idx}`);
790
- detail.classList.toggle('visible');
791
- btn.textContent = detail.classList.contains('visible') ? '−' : '+';
792
- });
793
-
794
- table.appendChild(tbody);
795
- // Replace table only, keep no-data div
796
- const oldTable = area.querySelector('table');
797
- if (oldTable) oldTable.remove();
798
- area.prepend(table);
799
- }
800
-
801
- function renderCaseTable(area) {
802
- const selected = allCaseData.filter(cd => selectedCases.has(cd.name));
803
- // Collect all unique timestamps where any selected case has data, newest first
804
- const tsSet = new Set();
805
- selected.forEach(cd => cd.entries.forEach(e => tsSet.add(e.timestamp)));
806
- const timestamps = [...tsSet].sort((a, b) => b.localeCompare(a));
807
-
808
- // Build score lookup per case: { timestamp -> score }
809
- const lookups = selected.map(cd => {
810
- const map = {};
811
- cd.entries.forEach(e => { map[e.timestamp] = e.score; });
812
- return map;
813
- });
814
-
815
- // Build prev-score lookup for deltas
816
- const prevLookups = selected.map(cd => {
817
- const map = {};
818
- for (let i = 1; i < cd.entries.length; i++) {
819
- map[cd.entries[i].timestamp] = cd.entries[i - 1].score;
820
- }
821
- return map;
822
- });
823
-
824
- const table = document.createElement('table');
825
- // Header: Date + one column per selected case (score + delta subcolumns)
826
- let headerHtml = '<thead><tr><th>Date</th>';
827
- selected.forEach(cd => {
828
- headerHtml += `<th><span class="th-case-dot" style="background:${cd.color}"></span>${cd.name}</th>`;
829
- headerHtml += `<th style="width:50px">\u0394</th>`;
830
- });
831
- headerHtml += '</tr></thead>';
832
- table.innerHTML = headerHtml;
833
-
834
- const tbody = document.createElement('tbody');
835
- timestamps.forEach(ts => {
836
- const tr = document.createElement('tr');
837
- let cells = `<td class="ts">${fmtDate(ts)}</td>`;
838
- selected.forEach((cd, i) => {
839
- const score = lookups[i][ts];
840
- const prev = prevLookups[i][ts];
841
- if (score != null) {
842
- const delta = prev != null ? score - prev : null;
843
- cells += `<td class="${scoreClass(score)}">${score.toFixed(1)}%</td>`;
844
- cells += `<td>${deltaHtml(delta)}</td>`;
845
- } else {
846
- cells += `<td class="d-zero">—</td><td></td>`;
847
- }
848
- });
849
- tr.innerHTML = cells;
850
- tbody.appendChild(tr);
851
- });
852
-
853
- table.appendChild(tbody);
854
- const oldTable = area.querySelector('table');
855
- if (oldTable) oldTable.remove();
856
- area.prepend(table);
857
- }
858
-
859
- // --- Button handlers ---
860
- document.getElementById('btn-clear').addEventListener('click', () => {
861
- selectedCases.clear();
862
- document.querySelectorAll('.case-item').forEach(el => {
863
- el.classList.remove('checked');
864
- el.querySelector('input').checked = false;
865
- });
866
- onSelectionChanged();
867
- });
868
-
869
- document.getElementById('btn-all').addEventListener('click', () => {
870
- allCaseData.forEach(cd => selectedCases.add(cd.name));
871
- document.querySelectorAll('.case-item').forEach(el => {
872
- el.classList.add('checked');
873
- el.querySelector('input').checked = true;
874
- });
875
- onSelectionChanged();
876
- });
877
-
878
- function onSelectionChanged() {
879
- redrawChart();
880
- renderTable();
881
- }
882
-
883
- // --- Init ---
884
- setTimeout(async () => {
885
- try {
886
- const runs = await loadHistory();
887
- allRuns = [...runs].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
888
- allCaseData = buildCaseData(runs);
889
- const latest = allRuns[allRuns.length - 1];
890
- if (latest) {
891
- document.getElementById('summary').textContent =
892
- latest.average.toFixed(1) + '% px avg / ' + runAveragePos(latest).toFixed(1) + '% pos avg / ' + latest.caseCount + ' cases';
893
- }
894
- renderCaseList();
895
- renderTable();
896
- redrawChart();
897
- } catch (e) {
898
- console.error('Failed to load history:', e);
899
- document.getElementById('no-data').style.display = '';
900
- document.getElementById('no-data').textContent = 'Error: ' + e.message;
901
- }
902
- }, 500);
903
-
904
- // Resize handling
905
- window.addEventListener('resize', () => redrawChart());
906
- </script>
907
- </body>
908
- </html>