ranger-compiler 3.5.0 → 3.5.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 (338) hide show
  1. package/CHANGELOG.md +1160 -19
  2. package/LICENSE +3 -1
  3. package/README.md +124 -29
  4. package/dist/Lang.rgr +1188 -290
  5. package/dist/api.d.ts +2343 -879
  6. package/dist/api.js +79924 -56922
  7. package/dist/lib/JSON.rgr +102 -91
  8. package/dist/lib/Shell.rgr +4 -4
  9. package/dist/lib/apple/AppleToolchain.rgr +4 -4
  10. package/dist/lib/apple/README.md +5 -4
  11. package/dist/lib/apple/apple_test.rgr +1 -1
  12. package/dist/lib/core/README.md +1 -1
  13. package/dist/lib/evg/EVG.rgr +12 -0
  14. package/dist/lib/evg/EVGA11yFromTree.rgr +302 -0
  15. package/dist/lib/evg/EVGA11yTree.rgr +894 -0
  16. package/dist/lib/evg/EVGBox.rgr +267 -0
  17. package/dist/lib/evg/EVGBoxShorthandTest.rgr +220 -0
  18. package/dist/lib/evg/EVGCodepoint.rgr +316 -0
  19. package/dist/lib/evg/EVGColor.rgr +700 -0
  20. package/dist/lib/evg/EVGCommands.rgr +177 -0
  21. package/dist/lib/evg/EVGComponent.rgr +331 -0
  22. package/dist/lib/evg/EVGComponentTest.rgr +387 -0
  23. package/dist/lib/evg/EVGConnector.rgr +541 -0
  24. package/dist/lib/evg/EVGConnectorTest.rgr +306 -0
  25. package/dist/lib/evg/EVGDisplayList.rgr +4186 -0
  26. package/dist/lib/evg/EVGEasing.rgr +370 -0
  27. package/dist/lib/evg/EVGEffectTest.rgr +253 -0
  28. package/dist/lib/evg/EVGElement.rgr +4914 -0
  29. package/dist/lib/evg/EVGFixedTest.rgr +289 -0
  30. package/dist/lib/evg/EVGFlexRulesTest.rgr +317 -0
  31. package/dist/lib/evg/EVGFlexWrapTest.rgr +246 -0
  32. package/dist/lib/evg/EVGFling.rgr +244 -0
  33. package/dist/lib/evg/EVGFocus.rgr +400 -0
  34. package/dist/lib/evg/EVGFocusTest.rgr +399 -0
  35. package/dist/lib/evg/EVGGradient.rgr +322 -0
  36. package/dist/lib/evg/EVGGrapheme.rgr +190 -0
  37. package/dist/lib/evg/EVGGrid.rgr +975 -0
  38. package/dist/lib/evg/EVGHitTest.rgr +208 -0
  39. package/dist/lib/evg/EVGHoles.rgr +294 -0
  40. package/dist/lib/evg/EVGHostMeasurerTest.rgr +281 -0
  41. package/dist/lib/evg/EVGHostTextMeasurer.rgr +331 -0
  42. package/dist/lib/evg/EVGHostTree.rgr +834 -0
  43. package/dist/lib/evg/EVGHostTreeTest.rgr +447 -0
  44. package/dist/lib/evg/EVGImageDecode.rgr +117 -0
  45. package/dist/lib/evg/EVGImageMeasurer.rgr +86 -0
  46. package/dist/lib/evg/EVGInspect.rgr +883 -0
  47. package/dist/lib/evg/EVGInvalidateTest.rgr +432 -0
  48. package/dist/lib/evg/EVGJsonTest.rgr +535 -0
  49. package/dist/lib/evg/EVGLayout.rgr +4220 -0
  50. package/dist/lib/evg/EVGMeasure.rgr +1267 -0
  51. package/dist/lib/evg/EVGOverlayTest.rgr +626 -0
  52. package/dist/lib/evg/EVGPatch.rgr +1721 -0
  53. package/dist/lib/evg/EVGPatchTest.rgr +692 -0
  54. package/dist/lib/evg/EVGPopoverTest.rgr +395 -0
  55. package/dist/lib/evg/EVGReconcile.rgr +226 -0
  56. package/dist/lib/evg/EVGReconcileTest.rgr +421 -0
  57. package/dist/lib/evg/EVGReject.rgr +126 -0
  58. package/dist/lib/evg/EVGRelayoutTest.rgr +375 -0
  59. package/dist/lib/evg/EVGRtlLayoutTest.rgr +319 -0
  60. package/dist/lib/evg/EVGRuler.rgr +232 -0
  61. package/dist/lib/evg/EVGRulerTest.rgr +172 -0
  62. package/dist/lib/evg/EVGSelectChrome.rgr +178 -0
  63. package/dist/lib/evg/EVGStyleCacheTest.rgr +419 -0
  64. package/dist/lib/evg/EVGStyleSheet.rgr +2148 -0
  65. package/dist/lib/evg/EVGStyleStateTest.rgr +407 -0
  66. package/dist/lib/evg/EVGStyleVarTest.rgr +471 -0
  67. package/dist/lib/evg/EVGText.rgr +105 -0
  68. package/dist/lib/evg/EVGTextEngine.rgr +536 -0
  69. package/dist/lib/evg/EVGTextMeasurer.rgr +720 -0
  70. package/dist/lib/evg/EVGTimingTest.rgr +1417 -0
  71. package/dist/lib/evg/EVGToolbar.rgr +1369 -0
  72. package/dist/lib/evg/EVGTransition.rgr +728 -0
  73. package/dist/lib/evg/EVGTreeJson.rgr +721 -0
  74. package/dist/lib/evg/EVGUnit.rgr +554 -0
  75. package/dist/lib/evg/EVGViewportUnitTest.rgr +236 -0
  76. package/dist/lib/evg/EvgApp.rgr +189 -0
  77. package/dist/lib/evg/EvgBitmapTracer.rgr +4277 -0
  78. package/dist/lib/evg/EvgBitmapTracerTest.rgr +2119 -0
  79. package/dist/lib/evg/EvgHost.rgr +451 -0
  80. package/dist/lib/evg/EvgTest.rgr +91 -0
  81. package/dist/lib/evg/EvgTraceColor.rgr +236 -0
  82. package/dist/lib/evg/EvgTraceCurve.rgr +648 -0
  83. package/dist/lib/evg/EvgTraceFit.rgr +767 -0
  84. package/dist/lib/evg/EvgTracePath.rgr +435 -0
  85. package/dist/lib/evg/EvgTraceTypes.rgr +559 -0
  86. package/dist/lib/evg/EvgViewport.rgr +313 -0
  87. package/dist/lib/evg/FxDemoDoc.rgr +185 -0
  88. package/dist/lib/evg/HOSTS.md +227 -0
  89. package/dist/lib/evg/ISSUES.md +835 -0
  90. package/dist/lib/evg/PLAN_ACCESSIBILITY.md +650 -0
  91. package/dist/lib/evg/PLAN_CSS_LAYOUT_AND_FONTS.md +1124 -0
  92. package/dist/lib/evg/PLAN_EFFECTS.md +431 -0
  93. package/dist/lib/evg/PLAN_EVG.md +518 -0
  94. package/dist/lib/evg/PLAN_EVG_RENDERER.md +1674 -0
  95. package/dist/lib/evg/PLAN_INSPECTOR.md +788 -0
  96. package/dist/lib/evg/PLAN_LINKS_AND_FORMS.md +157 -0
  97. package/dist/lib/evg/PLAN_NATIVE_HOSTS.md +671 -0
  98. package/dist/lib/evg/PLAN_VECTOR_IR.md +664 -0
  99. package/dist/lib/evg/PLAN_VIEW_TRANSFORM.md +322 -0
  100. package/dist/lib/evg/PathBuilder.rgr +333 -0
  101. package/dist/lib/evg/README.md +1236 -0
  102. package/dist/lib/evg/SPEC.md +1252 -0
  103. package/dist/lib/evg/SVGPathParser.rgr +1199 -0
  104. package/dist/lib/evg/SvgParser.rgr +1736 -0
  105. package/dist/lib/evg/TODO_EVG_RENDERER.md +595 -0
  106. package/dist/lib/evg/VectorShapes.rgr +331 -0
  107. package/dist/lib/evg/VectorStroke.rgr +107 -0
  108. package/dist/lib/evg/VectorViewBox.rgr +380 -0
  109. package/dist/lib/evg/agent/README.md +290 -0
  110. package/dist/lib/evg/agent/evg_agent.rgr +899 -0
  111. package/dist/lib/evg/agent/fixtures/broken.evg.json +7 -0
  112. package/dist/lib/evg/agent/fixtures/card.evg.json +9 -0
  113. package/dist/lib/evg/agent/fixtures/connector.css +63 -0
  114. package/dist/lib/evg/agent/fixtures/connector.evg.json +14 -0
  115. package/dist/lib/evg/agent/fixtures/drawn.evg.json +129 -0
  116. package/dist/lib/evg/agent/fixtures/gradient.evg.json +4 -0
  117. package/dist/lib/evg/agent/fixtures/popover.css +99 -0
  118. package/dist/lib/evg/agent/fixtures/popover.evg.json +30 -0
  119. package/dist/lib/evg/agent/roundtrip.sh +74 -0
  120. package/dist/lib/evg/agent/smoke.sh +230 -0
  121. package/dist/lib/evg/android/README.md +97 -0
  122. package/dist/lib/evg/android/androidstubs/AndroidStubs.kt +175 -0
  123. package/dist/lib/evg/android/androidstubs/Annotation.kt +10 -0
  124. package/dist/lib/evg/android/androidstubs/App.kt +30 -0
  125. package/dist/lib/evg/android/androidstubs/Content.kt +41 -0
  126. package/dist/lib/evg/android/androidstubs/ContentRes.kt +12 -0
  127. package/dist/lib/evg/android/androidstubs/InputMethod.kt +46 -0
  128. package/dist/lib/evg/android/androidstubs/Net.kt +6 -0
  129. package/dist/lib/evg/android/androidstubs/Os.kt +30 -0
  130. package/dist/lib/evg/android/androidstubs/Util.kt +15 -0
  131. package/dist/lib/evg/android/androidstubs/View.kt +113 -0
  132. package/dist/lib/evg/android/androidstubs/Widget.kt +16 -0
  133. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidEvgSurface.kt +323 -0
  134. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidTextMeasurer.kt +56 -0
  135. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/RippleEffect.kt +262 -0
  136. package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtEvgSurface.kt +238 -0
  137. package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtTextMeasurer.kt +85 -0
  138. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgEngineThread.kt +115 -0
  139. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgPainter.kt +231 -0
  140. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgSurface.kt +117 -0
  141. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/RecordingSurface.kt +94 -0
  142. package/dist/lib/evg/apple/README.md +128 -0
  143. package/dist/lib/evg/apple/Sources/CoreGraphicsEvgSurface.swift +329 -0
  144. package/dist/lib/evg/apple/Sources/CoreTextMeasurer.swift +70 -0
  145. package/dist/lib/evg/apple/Sources/EvgEngineQueue.swift +105 -0
  146. package/dist/lib/evg/apple/Sources/EvgPainter.swift +225 -0
  147. package/dist/lib/evg/apple/Sources/EvgSurface.swift +163 -0
  148. package/dist/lib/evg/apple/Sources/RecordingSurface.swift +110 -0
  149. package/dist/lib/evg/bench/EvgLayoutBench.rgr +34 -0
  150. package/dist/lib/evg/bench/README.md +64 -0
  151. package/dist/lib/evg/bench/layout-bench.mjs +316 -0
  152. package/dist/lib/evg/bench/layout-cases.mjs +510 -0
  153. package/dist/lib/evg/bench/layout-conformance.mjs +254 -0
  154. package/dist/lib/evg/bin/.gitignore +17 -0
  155. package/dist/lib/evg/evg_test.rgr +313 -0
  156. package/dist/lib/evg/gl/README.md +198 -0
  157. package/dist/lib/evg/gl/a11y-paint-check.mjs +73 -0
  158. package/dist/lib/evg/gl/blur-check.mjs +399 -0
  159. package/dist/lib/evg/gl/boxmodel.json +1 -0
  160. package/dist/lib/evg/gl/demo.html +46 -0
  161. package/dist/lib/evg/gl/effect-presets.css +285 -0
  162. package/dist/lib/evg/gl/effect-presets.js +63 -0
  163. package/dist/lib/evg/gl/effect-shots.mjs +135 -0
  164. package/dist/lib/evg/gl/evg-a11y.js +566 -0
  165. package/dist/lib/evg/gl/evg-binary.js +160 -0
  166. package/dist/lib/evg/gl/evg-engine.js +296 -0
  167. package/dist/lib/evg/gl/evg-fx.js +237 -0
  168. package/dist/lib/evg/gl/evg-gestures.js +209 -0
  169. package/dist/lib/evg/gl/evg-list.js +167 -0
  170. package/dist/lib/evg/gl/evg-measure.js +186 -0
  171. package/dist/lib/evg/gl/evg-textinput.js +303 -0
  172. package/dist/lib/evg/gl/evg-view.js +144 -0
  173. package/dist/lib/evg/gl/evg-webgl.js +3942 -0
  174. package/dist/lib/evg/gl/fx-check.mjs +998 -0
  175. package/dist/lib/evg/gl/fx-demo.html +166 -0
  176. package/dist/lib/evg/gl/fx-demo.js +2 -0
  177. package/dist/lib/evg/gl/fx-serve.mjs +47 -0
  178. package/dist/lib/evg/gl/gestures-check.mjs +189 -0
  179. package/dist/lib/evg/gl/list-binary-check.mjs +152 -0
  180. package/dist/lib/evg/gl/measure-check.mjs +135 -0
  181. package/dist/lib/evg/gl/rotation-check.mjs +207 -0
  182. package/dist/lib/evg/gl/shift-check.mjs +113 -0
  183. package/dist/lib/evg/gl/stroke-check.mjs +168 -0
  184. package/dist/lib/evg/gl/text-snap-check.mjs +139 -0
  185. package/dist/lib/evg/gl/view-check.mjs +343 -0
  186. package/dist/lib/evg/gl/view-policy-check.mjs +184 -0
  187. package/dist/lib/evg/html/evg-dom.js +318 -0
  188. package/dist/lib/evg/html/evg-html.js +600 -0
  189. package/dist/lib/evg/inspect/README.md +415 -0
  190. package/dist/lib/evg/inspect/browser-smoke.mjs +115 -0
  191. package/dist/lib/evg/inspect/evg-inspect.js +947 -0
  192. package/dist/lib/evg/inspect/shots/css.png +0 -0
  193. package/dist/lib/evg/inspect/shots/dashboard.png +0 -0
  194. package/dist/lib/evg/inspect/shots/pptx-slide.png +0 -0
  195. package/dist/lib/evg/inspect/shots/state.png +0 -0
  196. package/dist/lib/evg/inspect/shots.mjs +226 -0
  197. package/dist/lib/evg/oracle/css-blur.json +576 -0
  198. package/dist/lib/evg/oracle/css-box.json +157 -0
  199. package/dist/lib/evg/oracle/css-timing.json +709 -0
  200. package/dist/lib/evg/oracle/css_blur_oracle.mjs +529 -0
  201. package/dist/lib/evg/oracle/css_box_oracle.mjs +106 -0
  202. package/dist/lib/evg/oracle/css_timing_oracle.mjs +389 -0
  203. package/dist/lib/evg/original/EVGColor.clj +310 -0
  204. package/dist/lib/evg/original/EVGColorContext.rgr +178 -0
  205. package/dist/lib/evg/original/SVGPath.rgr +627 -0
  206. package/dist/lib/evg/original/Vec2.crgr +103 -0
  207. package/dist/lib/evg/ranger.json +9 -0
  208. package/dist/lib/evg/showcase/README.md +246 -0
  209. package/dist/lib/evg/showcase/assets/emblem.svg +30 -0
  210. package/dist/lib/evg/showcase/assets/rosette.svg +22 -0
  211. package/dist/lib/evg/showcase/build.mjs +651 -0
  212. package/dist/lib/evg/showcase/pages/album.tsx +30 -0
  213. package/dist/lib/evg/showcase/pages/boxmodel.tsx +37 -0
  214. package/dist/lib/evg/showcase/pages/cards.tsx +41 -0
  215. package/dist/lib/evg/showcase/pages/chart_api.tsx +170 -0
  216. package/dist/lib/evg/showcase/pages/charts.tsx +219 -0
  217. package/dist/lib/evg/showcase/pages/drawing.tsx +160 -0
  218. package/dist/lib/evg/showcase/pages/emoji.tsx +91 -0
  219. package/dist/lib/evg/showcase/pages/flex.tsx +46 -0
  220. package/dist/lib/evg/showcase/pages/more.tsx +332 -0
  221. package/dist/lib/evg/showcase/pages/plots.tsx +262 -0
  222. package/dist/lib/evg/showcase/pages/svg.tsx +63 -0
  223. package/dist/lib/evg/showcase/pages/tables.tsx +209 -0
  224. package/dist/lib/evg/showcase/pages/typography.tsx +43 -0
  225. package/dist/lib/evg/showcase/pages/units.tsx +35 -0
  226. package/dist/lib/evg/showcase/pages/variants.tsx +233 -0
  227. package/dist/lib/evg/showcase/pages/vector.tsx +75 -0
  228. package/dist/lib/evg/showcase/pages/views.tsx +223 -0
  229. package/dist/lib/evg/showcase/tests/chart_api_smoke.mjs +339 -0
  230. package/dist/lib/evg/showcase/tests/gl_smoke.mjs +125 -0
  231. package/dist/lib/evg/showcase/themes/chart_api-default.css +15 -0
  232. package/dist/lib/evg/showcase/themes/charts-default.css +29 -0
  233. package/dist/lib/evg/showcase/themes/drawing-default.css +35 -0
  234. package/dist/lib/evg/showcase/themes/more-default.css +128 -0
  235. package/dist/lib/evg/showcase/themes/plots-default.css +18 -0
  236. package/dist/lib/evg/showcase/themes/showcase.css +508 -0
  237. package/dist/lib/evg/showcase/themes/tables-default.css +123 -0
  238. package/dist/lib/evg/showcase/themes/variants-default.css +15 -0
  239. package/dist/lib/evg/showcase/themes/views-default.css +15 -0
  240. package/dist/lib/evg/tools/bench_vs_potrace.mjs +290 -0
  241. package/dist/lib/evg/tools/evg_image_tool.rgr +488 -0
  242. package/dist/lib/evg/tools/evg_trace_bench.rgr +114 -0
  243. package/dist/lib/evg/tools/evg_trace_cli.rgr +662 -0
  244. package/dist/lib/evg/tools/evg_trace_cpp_bench.rgr +83 -0
  245. package/dist/lib/evg/tools/run_trace_bench.sh +74 -0
  246. package/dist/lib/evg/tools/run_trace_cli_smoke.sh +128 -0
  247. package/dist/lib/evg/web/responsive/EvgResponsiveCheck.rgr +271 -0
  248. package/dist/lib/evg/web/responsive/EvgResponsiveDemo.rgr +531 -0
  249. package/dist/lib/evg/web/responsive/README.md +97 -0
  250. package/dist/lib/evg/web/responsive/build.mjs +90 -0
  251. package/dist/lib/evg/web/responsive/dom-check.mjs +169 -0
  252. package/dist/lib/evg/web/responsive/index.html +177 -0
  253. package/dist/lib/evg/web/responsive/smoke.mjs +221 -0
  254. package/dist/lib/evg/web/tools/assets-client.mjs +95 -0
  255. package/dist/lib/evg/web/tools/boot-bench.mjs +189 -0
  256. package/dist/lib/evg/web/tools/inline-assets.mjs +119 -0
  257. package/dist/lib/evg/web/tools/minify.mjs +60 -0
  258. package/dist/lib/evg/web/tracer/build.mjs +87 -0
  259. package/dist/lib/evg/web/tracer/index.html +2805 -0
  260. package/dist/lib/evg/web/tracer/sample.png +0 -0
  261. package/dist/lib/evg/web/tracer/smoke.mjs +1162 -0
  262. package/dist/lib/evgr/Cargo.lock +21 -0
  263. package/dist/lib/evgr/Cargo.toml +19 -0
  264. package/dist/lib/evgr/README.md +140 -0
  265. package/dist/lib/evgr/bench/NativeBench.rgr +246 -0
  266. package/dist/lib/evgr/bench/compare.mjs +252 -0
  267. package/dist/lib/evgr/bench/speed.mjs +277 -0
  268. package/dist/lib/evgr/bin/.gitignore +3 -0
  269. package/dist/lib/evgr/src/bin/bench.rs +106 -0
  270. package/dist/lib/evgr/src/bin/smoke.rs +13 -0
  271. package/dist/lib/evgr/src/grid.rs +730 -0
  272. package/dist/lib/evgr/src/lib.rs +1003 -0
  273. package/dist/lib/evgr/src/style.rs +468 -0
  274. package/dist/lib/evgr/src/text.rs +83 -0
  275. package/dist/lib/image/BitReader.rgr +171 -0
  276. package/dist/lib/image/Buffer.rgr +173 -0
  277. package/dist/lib/image/DCT.rgr +283 -0
  278. package/dist/lib/image/Deflate.rgr +339 -0
  279. package/dist/lib/image/HuffmanDecoder.rgr +181 -0
  280. package/dist/lib/image/ImageBuffer.rgr +706 -0
  281. package/dist/lib/image/JPEGDecoder.rgr +808 -0
  282. package/dist/lib/image/PNGDecoder.rgr +544 -0
  283. package/dist/lib/image/PNGEncoder.rgr +388 -0
  284. package/dist/lib/image/PPMImage.rgr +224 -0
  285. package/dist/lib/image/ProgressiveJPEGDecoder.rgr +1154 -0
  286. package/dist/lib/image/README.md +33 -0
  287. package/dist/lib/image/RasterBuffer.rgr +294 -0
  288. package/dist/lib/image/VP8BoolDecoder.rgr +163 -0
  289. package/dist/lib/image/WebPDecoder.rgr +433 -0
  290. package/dist/lib/image/WebPLossless.rgr +1035 -0
  291. package/dist/lib/image/WebPLossy.rgr +1929 -0
  292. package/dist/lib/image/ranger.json +9 -0
  293. package/dist/lib/image/testdata/webp/anim_2f_24x16.webp +0 -0
  294. package/dist/lib/image/testdata/webp/fixtures.txt +33 -0
  295. package/dist/lib/image/testdata/webp/gen_fixtures.py +261 -0
  296. package/dist/lib/image/testdata/webp/ll_1x1.webp +0 -0
  297. package/dist/lib/image/testdata/webp/ll_alpha_29x21.webp +0 -0
  298. package/dist/lib/image/testdata/webp/ll_gradient_64x64.webp +0 -0
  299. package/dist/lib/image/testdata/webp/ll_meta_32x32.webp +0 -0
  300. package/dist/lib/image/testdata/webp/ll_noise_17x9.webp +0 -0
  301. package/dist/lib/image/testdata/webp/ll_pal11_23x11.webp +0 -0
  302. package/dist/lib/image/testdata/webp/ll_pal2_19x7.webp +0 -0
  303. package/dist/lib/image/testdata/webp/ll_pal40_16x16.webp +0 -0
  304. package/dist/lib/image/testdata/webp/ll_pal4_13x5.webp +0 -0
  305. package/dist/lib/image/testdata/webp/ll_photo_40x30.webp +0 -0
  306. package/dist/lib/image/testdata/webp/ll_predictors_64x32.webp +0 -0
  307. package/dist/lib/image/testdata/webp/ly_1x1.webp +0 -0
  308. package/dist/lib/image/testdata/webp/ly_alph_raw_f0_21x13.webp +0 -0
  309. package/dist/lib/image/testdata/webp/ly_alph_raw_f1_21x13.webp +0 -0
  310. package/dist/lib/image/testdata/webp/ly_alph_raw_f2_21x13.webp +0 -0
  311. package/dist/lib/image/testdata/webp/ly_alph_raw_f3_21x13.webp +0 -0
  312. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f0_21x13.webp +0 -0
  313. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f1_21x13.webp +0 -0
  314. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f2_21x13.webp +0 -0
  315. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f3_21x13.webp +0 -0
  316. package/dist/lib/image/testdata/webp/ly_alpha_33x17.webp +0 -0
  317. package/dist/lib/image/testdata/webp/ly_exif_15x11.webp +0 -0
  318. package/dist/lib/image/testdata/webp/ly_i16_96x80.webp +0 -0
  319. package/dist/lib/image/testdata/webp/ly_nofilter_24x20.webp +0 -0
  320. package/dist/lib/image/testdata/webp/ly_odd_17x9.webp +0 -0
  321. package/dist/lib/image/testdata/webp/ly_photo_48x40.webp +0 -0
  322. package/dist/lib/image/testdata/webp/ly_q100_20x18.webp +0 -0
  323. package/dist/lib/image/testdata/webp/ly_sharp_40x36.webp +0 -0
  324. package/dist/lib/image/testdata/webp/ly_simple_filter_33x31.webp +0 -0
  325. package/dist/lib/image/testdata/webp/ly_vpx_parts8_37x45.webp +0 -0
  326. package/dist/lib/image/testdata/webp/ly_vpx_skip_96x80.webp +0 -0
  327. package/dist/lib/image/tests/WebPDecodeTool.rgr +81 -0
  328. package/dist/lib/image/tests/WebPDecoderTest.rgr +1162 -0
  329. package/dist/lib/image/tests/run_webp_tests.sh +39 -0
  330. package/dist/lib/rust/RsJson.rgr +468 -0
  331. package/dist/lib/rust/RsPrelude.rgr +1663 -0
  332. package/dist/lib/shell_test.rgr +7 -7
  333. package/dist/lib/stdlib.rgr +21 -5
  334. package/dist/lib/zip/ranger.json +6 -0
  335. package/dist/rgrc.js +87318 -67610
  336. package/package.json +1010 -1040
  337. package/dist/README.md +0 -117
  338. package/dist/package.json +0 -47
@@ -0,0 +1,947 @@
1
+ // SPDX-License-Identifier: MIT
2
+ //
3
+ // The EVG inspector panel — dev tools for a picture nobody can read.
4
+ //
5
+ // An EVG app in a browser is one <canvas>, or a flat pile of <rect> and <path>
6
+ // in paint order. Either way the browser's own dev tools have nothing to say
7
+ // about it. This is the panel that does: the element hierarchy, the box model
8
+ // over the real pixels, the computed style, and the draw commands each element
9
+ // emitted.
10
+ //
11
+ // GENERIC ON PURPOSE. It knows nothing about any app. It is handed an adapter
12
+ // of small functions and everything it shows is read through them:
13
+ //
14
+ // attach({
15
+ // surface, // the <canvas> or <svg> the app paints into
16
+ // app: {
17
+ // tree: () => json, // REQUIRED — EVGInspect.treeJson()
18
+ // node: (path) => json, // optional — EVGInspect.nodeJson(path)
19
+ // hit: (x, y) => path, // optional — EVGInspect.hitPath(); a geometric
20
+ // // fallback is used when absent
21
+ // frame: () => json, // optional — displayListJson() with attribution
22
+ // refresh: () => void, // optional — ask the host to repaint
23
+ // viewport: () => [w, h], // optional — the surface's logical size, when
24
+ // // the tree does not fill it
25
+ // transform: () => json, // optional — {x, y, k} mapping the tree's
26
+ // // coordinates onto the surface
27
+ // css: () => ({ name, text, href, errors }), // optional
28
+ // setCss: (text) => void, // optional
29
+ // force: (path, bits) => void, // optional — hold a
30
+ // // state on: 1 hover, 2 focus,
31
+ // // 4 pressed, 8 disabled
32
+ // saveCss: (text) => void, // optional — write it
33
+ // // back where it came from
34
+ // label: "…", // optional — what to call the app
35
+ // },
36
+ // })
37
+ //
38
+ // EVERY ONE OF THESE MAY RETURN A PROMISE. A host in the same page answers
39
+ // synchronously and nothing here notices the difference; the three directions
40
+ // that are not in the same page — a DevTools extension over
41
+ // `inspectedWindow.eval`, a native target over a socket, a paused page
42
+ // answering out of a snapshot — are all the same shape, which is that the
43
+ // answer does not come back on the same tick. Awaiting a value that is already
44
+ // a value costs a microtask, and writing the panel as if it could not is the
45
+ // kind of decision that is cheap now and a rewrite later.
46
+ //
47
+ // The two exceptions are `transform` and `viewport`, which are read on every
48
+ // pointer move to place the overlay. Those are awaited once per refresh and
49
+ // cached, because a highlight that arrives a tick after the pointer is a
50
+ // highlight that lags.
51
+ //
52
+ // Everything past `tree` degrades rather than fails, which is what makes one
53
+ // panel serve a slide editor and a dashboard without either knowing about it.
54
+ //
55
+ // `css`/`setCss` are the pair that makes the panel an editor rather than a
56
+ // viewer, and they are the reason it does not need an override layer. The
57
+ // element tree is an app's OUTPUT — edit it and the next rebuild throws the
58
+ // edit away, which is why a DevTools style edit dies on a React re-render. The
59
+ // stylesheet is an app's INPUT: hand back a changed one and the app re-parses
60
+ // and re-cascades exactly as it did at startup. Nothing is intercepted, and
61
+ // the text in the editor is the text that goes in the file.
62
+ //
63
+ // The overlay is DOM over the surface, not commands in the display list. Two
64
+ // reasons and both matter: a screenshot taken while the panel is open is still
65
+ // a screenshot of the app, and one implementation then serves both painters —
66
+ // the WebGL canvas and the SVG one are the same rectangle on the page.
67
+
68
+ const CSS = `
69
+ .evgi { position: fixed; top: 0; right: 0; bottom: 0; width: 420px; z-index: 2147483000;
70
+ display: flex; flex-direction: column; box-sizing: border-box;
71
+ font: 12px/1.5 ui-sans-serif, system-ui, "Segoe UI", sans-serif;
72
+ background: var(--evgi-bg); color: var(--evgi-ink); border-left: 1px solid var(--evgi-line);
73
+ box-shadow: -8px 0 28px rgba(0,0,0,.28); }
74
+ .evgi, .evgi * { box-sizing: border-box; }
75
+ .evgi { --evgi-bg:#12151c; --evgi-panel:#171b24; --evgi-line:#262c39; --evgi-ink:#dfe4ee;
76
+ --evgi-dim:#8b96ab; --evgi-accent:#7aa2f7; --evgi-sel:#243044;
77
+ --evgi-margin:rgba(246,178,107,.45); --evgi-border:rgba(255,229,143,.45);
78
+ --evgi-padding:rgba(147,196,125,.40); --evgi-content:rgba(122,162,247,.42); }
79
+ .evgi.evgi-light { --evgi-bg:#ffffff; --evgi-panel:#f6f7f9; --evgi-line:#e2e5ea; --evgi-ink:#1b2029;
80
+ --evgi-dim:#6a7382; --evgi-accent:#3661c9; --evgi-sel:#e6ecf9; }
81
+ .evgi-head { display:flex; align-items:center; gap:6px; padding:7px 9px; border-bottom:1px solid var(--evgi-line);
82
+ background: var(--evgi-panel); flex: 0 0 auto; }
83
+ .evgi-title { font-weight:600; margin-right:auto; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
84
+ .evgi-btn { background:transparent; color:var(--evgi-ink); border:1px solid var(--evgi-line); border-radius:5px;
85
+ padding:3px 7px; cursor:pointer; font:inherit; line-height:1.3; }
86
+ .evgi-btn:hover { background: var(--evgi-sel); }
87
+ .evgi-btn.on { background: var(--evgi-accent); border-color: var(--evgi-accent); color:#0b0e14; }
88
+ .evgi-search { flex:0 0 auto; padding:6px 9px; border-bottom:1px solid var(--evgi-line); display:flex; gap:6px; }
89
+ .evgi-search input { flex:1; background:var(--evgi-bg); color:var(--evgi-ink); border:1px solid var(--evgi-line);
90
+ border-radius:5px; padding:4px 7px; font:inherit; }
91
+ .evgi-search input:focus { outline:none; border-color:var(--evgi-accent); }
92
+ .evgi-tree { flex: 1 1 46%; overflow:auto; padding:4px 0; min-height:80px; }
93
+ .evgi-row { display:flex; align-items:center; gap:4px; padding:1px 8px 1px 0; cursor:default; white-space:nowrap;
94
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:11.5px; }
95
+ .evgi-row:hover { background: rgba(122,162,247,.10); }
96
+ .evgi-row.sel { background: var(--evgi-sel); }
97
+ .evgi-tw { width:13px; flex:0 0 13px; text-align:center; color:var(--evgi-dim); cursor:pointer; user-select:none; }
98
+ .evgi-tag { color:#7ee0c0; }
99
+ .evgi-id { color:#f2b661; }
100
+ .evgi-cls { color:#b39ddb; }
101
+ .evgi-txt { color:var(--evgi-dim); font-style:italic; overflow:hidden; text-overflow:ellipsis; }
102
+ .evgi-dim { color: var(--evgi-dim); }
103
+ .evgi-tabs { display:flex; gap:2px; padding:5px 8px 0; border-top:1px solid var(--evgi-line); background:var(--evgi-panel); flex:0 0 auto; }
104
+ .evgi-tab { padding:4px 9px; border:1px solid transparent; border-bottom:none; border-radius:5px 5px 0 0;
105
+ cursor:pointer; color:var(--evgi-dim); }
106
+ .evgi-tab.on { background:var(--evgi-bg); border-color:var(--evgi-line); color:var(--evgi-ink); }
107
+ .evgi-body { flex:1 1 54%; overflow:auto; padding:9px 10px 14px; background:var(--evgi-bg); }
108
+ .evgi-body h4 { margin:12px 0 5px; font-size:10.5px; text-transform:uppercase; letter-spacing:.08em; color:var(--evgi-dim); font-weight:600; }
109
+ .evgi-body h4:first-child { margin-top:0; }
110
+ .evgi-kv { display:grid; grid-template-columns: minmax(96px,auto) 1fr; gap:1px 10px; font-family: ui-monospace, Menlo, monospace; font-size:11.5px; }
111
+ .evgi-kv > div { padding:1px 0; overflow-wrap:anywhere; }
112
+ .evgi-kv .k { color:var(--evgi-dim); }
113
+ .evgi-kv .v.ov { color:var(--evgi-accent); }
114
+ .evgi-swatch { display:inline-block; width:9px; height:9px; border-radius:2px; margin-right:5px;
115
+ border:1px solid rgba(128,128,128,.5); vertical-align:baseline; }
116
+ .evgi-boxmodel { margin:2px 0 6px; text-align:center; font-family: ui-monospace, Menlo, monospace; font-size:10.5px; }
117
+ .evgi-bm { padding:14px; position:relative; }
118
+ .evgi-bm > .lbl { position:absolute; top:2px; left:5px; font-size:9px; letter-spacing:.05em; color:var(--evgi-ink); opacity:.75; }
119
+ .evgi-bm.m { background: var(--evgi-margin); }
120
+ .evgi-bm.b { background: var(--evgi-border); }
121
+ .evgi-bm.p { background: var(--evgi-padding); }
122
+ .evgi-bm.c { background: var(--evgi-content); padding:16px 10px; }
123
+ .evgi-empty { color:var(--evgi-dim); padding:14px 2px; }
124
+ .evgi-rule { border:1px solid var(--evgi-line); border-radius:6px; margin:0 0 7px; overflow:hidden; }
125
+ .evgi-rule > header { display:flex; align-items:center; gap:7px; padding:4px 8px; background:var(--evgi-panel);
126
+ font-family: ui-monospace, Menlo, monospace; font-size:11.5px; }
127
+ .evgi-rule > header .sel { color:#b39ddb; }
128
+ .evgi-rule > header .cnt { margin-left:auto; color:var(--evgi-dim); font-size:10.5px; white-space:nowrap; }
129
+ .evgi-rule > header .media { color:#f2b661; font-size:10.5px; }
130
+ .evgi-rule .decls { padding:4px 8px 5px; display:grid; grid-template-columns:minmax(96px,auto) 1fr;
131
+ gap:1px 10px; font-family: ui-monospace, Menlo, monospace; font-size:11.5px; }
132
+ .evgi-rule .decls .k { color:var(--evgi-dim); }
133
+ .evgi-rule .lost .k, .evgi-rule .lost .v { text-decoration: line-through; opacity:.45; }
134
+ .evgi-rule .inl .k, .evgi-rule .inl .v { text-decoration: line-through; opacity:.55; }
135
+ .evgi-chip { display:inline-flex; align-items:center; gap:5px; border:1px solid var(--evgi-line);
136
+ border-radius:11px; padding:1px 8px; margin:0 5px 5px 0; font-family: ui-monospace, Menlo, monospace;
137
+ font-size:11px; }
138
+ .evgi-chip b { color:#b39ddb; font-weight:500; }
139
+ .evgi-hov { display:flex; gap:5px; flex-wrap:wrap; padding:0 0 2px; }
140
+ .evgi-hov button { font:11px ui-monospace, Menlo, monospace; padding:2px 8px; border-radius:5px;
141
+ border:1px solid var(--evgi-line); background:transparent; color:var(--evgi-dim); cursor:pointer; }
142
+ .evgi-hov button:hover { color:var(--evgi-ink); }
143
+ .evgi-hov button.on { background:var(--evgi-accent); border-color:var(--evgi-accent); color:#0b0e14; }
144
+ .evgi-hov .rel { margin-left:auto; }
145
+ .evgi-chip span { color:var(--evgi-dim); font-size:10.5px; }
146
+ .evgi-note { color:var(--evgi-dim); padding:6px 0 0; line-height:1.6; }
147
+ .evgi-css textarea { width:100%; min-height:230px; resize:vertical; background:var(--evgi-bg);
148
+ color:var(--evgi-ink); border:1px solid var(--evgi-line); border-radius:6px; padding:7px 8px;
149
+ font:11.5px/1.55 ui-monospace, Menlo, monospace; white-space:pre; overflow-wrap:normal; }
150
+ .evgi-css textarea:focus { outline:none; border-color:var(--evgi-accent); }
151
+ .evgi-css .bar { display:flex; align-items:center; gap:7px; padding:0 0 6px; }
152
+ .evgi-css .bar .name { font-family: ui-monospace, Menlo, monospace; font-size:11.5px; }
153
+ .evgi-css .bar .live { margin-left:auto; color:#7ee0c0; font-size:10.5px; }
154
+ .evgi-css .err { color:#f87171; font-family: ui-monospace, Menlo, monospace; font-size:11px; padding:5px 0 0; }
155
+ .evgi-cmd { font-family: ui-monospace, Menlo, monospace; font-size:11px; padding:1px 0; }
156
+ .evgi-cmd b { color:#7ee0c0; font-weight:600; }
157
+ .evgi-foot { flex:0 0 auto; padding:5px 9px; border-top:1px solid var(--evgi-line); color:var(--evgi-dim);
158
+ background:var(--evgi-panel); display:flex; gap:8px; align-items:center; }
159
+ .evgi-foot code { background:rgba(128,128,128,.16); padding:0 4px; border-radius:3px; }
160
+
161
+ .evgi-ov { position:absolute; pointer-events:none; z-index:2147482000; }
162
+ .evgi-ov > div { position:absolute; }
163
+ .evgi-ov .m { background: var(--evgi-margin, rgba(246,178,107,.45)); }
164
+ .evgi-ov .b { background: var(--evgi-border, rgba(255,229,143,.45)); }
165
+ .evgi-ov .p { background: var(--evgi-padding, rgba(147,196,125,.40)); }
166
+ .evgi-ov .c { background: var(--evgi-content, rgba(122,162,247,.42)); }
167
+ .evgi-ov .tip { background:#12151c; color:#dfe4ee; border:1px solid #39415a; border-radius:4px;
168
+ padding:2px 6px; font:11px/1.45 ui-monospace, Menlo, monospace; white-space:nowrap;
169
+ box-shadow:0 3px 10px rgba(0,0,0,.4); }
170
+ .evgi-picking { cursor: crosshair !important; }
171
+ `;
172
+
173
+ const el = (tag, cls, text) => {
174
+ const n = document.createElement(tag);
175
+ if (cls) n.className = cls;
176
+ if (text != null) n.textContent = text;
177
+ return n;
178
+ };
179
+ const parse = (v) => (typeof v === "string" ? JSON.parse(v) : v);
180
+ const px = (v) => (Math.round(v * 100) / 100);
181
+
182
+ // The overlay. Four absolutely positioned rings over the surface, in the four
183
+ // colours a browser uses — there is no reason to invent different ones — plus
184
+ // a label that says what is under them.
185
+ function makeOverlay() {
186
+ const host = el("div", "evgi-ov");
187
+ const parts = {};
188
+ for (const k of ["m", "b", "p", "c"]) { parts[k] = el("div", k); host.appendChild(parts[k]); }
189
+ const tip = el("div", "tip");
190
+ host.appendChild(tip);
191
+ host.style.display = "none";
192
+ return {
193
+ host,
194
+ hide() { host.style.display = "none"; },
195
+ // `rect` is the surface's position on the page; `scale` maps app pixels to
196
+ // CSS pixels, which is how a zoomed slide and a 1:1 dashboard share this.
197
+ // `t` maps box space to the surface's own logical space — identity for a
198
+ // page whose tree IS the surface, and a fit-and-centre for a slide drawn
199
+ // inside an editor window. `scale` then maps that to CSS pixels.
200
+ show(node, rect, scale, t = { x: 0, y: 0, k: 1 }) {
201
+ if (!node) { this.hide(); return; }
202
+ const k = t.k || 1;
203
+ const [x0, y0, w0, h0] = node.box;
204
+ const x = t.x + x0 * k, y = t.y + y0 * k, w = w0 * k, h = h0 * k;
205
+ const m = (node.m || [0, 0, 0, 0]).map((v) => v * k);
206
+ const b = (typeof node.b === "number" ? node.b : 0) * k;
207
+ const pd = (node.pd || [0, 0, 0, 0]).map((v) => v * k);
208
+ const S = (v) => v * scale;
209
+ const put = (n, l, t, ww, hh) => {
210
+ n.style.left = S(l) + "px"; n.style.top = S(t) + "px";
211
+ n.style.width = Math.max(0, S(ww)) + "px"; n.style.height = Math.max(0, S(hh)) + "px";
212
+ };
213
+ put(parts.m, x - m[3], y - m[0], w + m[1] + m[3], h + m[0] + m[2]);
214
+ put(parts.b, x, y, w, h);
215
+ put(parts.p, x + b, y + b, w - 2 * b, h - 2 * b);
216
+ put(parts.c, x + b + pd[3], y + b + pd[0],
217
+ w - 2 * b - pd[1] - pd[3], h - 2 * b - pd[0] - pd[2]);
218
+ const name = (node.tag || "?") + (node.tid ? "#" + node.tid : "")
219
+ + (node.cls ? "." + node.cls.split(/\s+/)[0] : "");
220
+ tip.textContent = `${name} ${px(w0)} × ${px(h0)}`;
221
+ tip.style.left = S(x) + "px";
222
+ const above = y * scale > 22;
223
+ tip.style.top = (above ? S(y) - 22 : S(y + h) + 4) + "px";
224
+ host.style.display = "";
225
+ host.style.left = "0px"; host.style.top = "0px";
226
+ host.style.width = rect.width + "px"; host.style.height = rect.height + "px";
227
+ },
228
+ };
229
+ }
230
+
231
+ export function attach(opts = {}) {
232
+ const app = opts.app || {};
233
+ if (typeof app.tree !== "function") throw new Error("evg-inspect: app.tree() is required");
234
+ const surface = opts.surface || document.querySelector("canvas") || document.querySelector("svg");
235
+ if (!surface) throw new Error("evg-inspect: no surface to overlay");
236
+
237
+ if (!document.getElementById("evgi-css")) {
238
+ const st = el("style"); st.id = "evgi-css"; st.textContent = CSS;
239
+ document.head.appendChild(st);
240
+ }
241
+
242
+ // --- state ---------------------------------------------------------------
243
+ let tree = null; // the last fetched tree
244
+ let byId = new Map();
245
+ let kids = new Map();
246
+ let selected = null; // node id
247
+ let detail = null; // node detail json
248
+ let frame = null; // display list, when the app offers one
249
+ let expanded = new Set(["0"]);
250
+ let picking = false;
251
+ let tab = "box";
252
+ let filter = "";
253
+
254
+ // --- panel ---------------------------------------------------------------
255
+ const panel = el("div", "evgi");
256
+ if (opts.theme === "light") panel.classList.add("evgi-light");
257
+
258
+ const head = el("div", "evgi-head");
259
+ const title = el("div", "evgi-title", app.label || "EVG inspector");
260
+ const pick = el("button", "evgi-btn", "⌖ pick");
261
+ pick.title = "Click an element in the picture (Esc to stop)";
262
+ const reload = el("button", "evgi-btn", "↻");
263
+ reload.title = "Re-read the tree";
264
+ const close = el("button", "evgi-btn", "✕");
265
+ head.append(title, pick, reload, close);
266
+
267
+ const searchBar = el("div", "evgi-search");
268
+ const search = el("input");
269
+ search.placeholder = "filter by tag, #id, .class or text";
270
+ searchBar.append(search);
271
+
272
+ const treeView = el("div", "evgi-tree");
273
+ const tabs = el("div", "evgi-tabs");
274
+ const body = el("div", "evgi-body");
275
+ const foot = el("div", "evgi-foot");
276
+
277
+ const TABS = [["box", "Box"], ["styles", "Styles"], ["css", "CSS"], ["cmds", "Commands"], ["raw", "Node"]];
278
+ const tabEls = {};
279
+ for (const [key, label] of TABS) {
280
+ const t = el("div", "evgi-tab", label);
281
+ t.onclick = () => { tab = key; renderTabs(); renderBody(); };
282
+ tabEls[key] = t;
283
+ tabs.appendChild(t);
284
+ }
285
+ panel.append(head, searchBar, treeView, tabs, body, foot);
286
+ document.body.appendChild(panel);
287
+
288
+ // The overlay goes inside the surface's offset parent so it scrolls with the
289
+ // picture rather than floating over the page.
290
+ const holder = surface.parentElement || document.body;
291
+ const holderPos = getComputedStyle(holder).position;
292
+ if (holderPos === "static") holder.style.position = "relative";
293
+ const overlay = makeOverlay();
294
+ holder.appendChild(overlay.host);
295
+
296
+ // --- reading the app -----------------------------------------------------
297
+
298
+ function surfaceRect() { return surface.getBoundingClientRect(); }
299
+
300
+ // The surface's own logical size and the tree's placement on it, read once
301
+ // per refresh and kept. See the note at the top about why these two, alone,
302
+ // are not awaited per call.
303
+ let view = { vw: 1, vh: 1, t: { x: 0, y: 0, k: 1 } };
304
+
305
+ async function readView() {
306
+ let vw = tree ? tree.w : 1;
307
+ let vh = tree ? tree.h : 1;
308
+ if (typeof app.viewport === "function") {
309
+ try { const v = await app.viewport(); if (v && v[0]) { vw = v[0]; vh = v[1]; } } catch { /* keep the tree's */ }
310
+ }
311
+ let t = { x: 0, y: 0, k: 1 };
312
+ if (typeof app.transform === "function") {
313
+ try {
314
+ const raw = parse(await app.transform());
315
+ if (raw && typeof raw.k === "number") t = { x: raw.x || 0, y: raw.y || 0, k: raw.k || 1 };
316
+ } catch { /* identity */ }
317
+ }
318
+ view = { vw, vh, t };
319
+ }
320
+
321
+ function viewport() { return [view.vw, view.vh]; }
322
+ function scale() {
323
+ const r = surfaceRect();
324
+ return r.width / view.vw || 1;
325
+ }
326
+ function transform() { return view.t; }
327
+
328
+ // Refreshes overlap when a host is slow — a repaint asks while the previous
329
+ // answer is still in flight — so the newest wins and the older ones drop
330
+ // their results rather than painting a tree that has already been replaced.
331
+ let refreshToken = 0;
332
+
333
+ async function refreshTree(keepSelection = true) {
334
+ const mine = ++refreshToken;
335
+ let fetched;
336
+ try {
337
+ fetched = parse(await app.tree());
338
+ } catch (e) {
339
+ if (mine !== refreshToken) return;
340
+ tree = null;
341
+ foot.textContent = "tree() failed: " + e.message;
342
+ return;
343
+ }
344
+ if (mine !== refreshToken) return;
345
+ tree = fetched;
346
+ byId = new Map();
347
+ kids = new Map();
348
+ for (const n of tree.nodes) {
349
+ byId.set(n.id, n);
350
+ if (n.p != null) {
351
+ if (!kids.has(n.p)) kids.set(n.p, []);
352
+ kids.get(n.p).push(n.id);
353
+ }
354
+ }
355
+ if (!keepSelection || (selected && !byId.has(selected))) {
356
+ // A path that stopped resolving is a dropped selection, said out loud.
357
+ // Re-pointing it at whatever now sits at that index would be a lie.
358
+ if (selected) foot.dataset.note = "selection dropped: " + selected;
359
+ selected = null; detail = null;
360
+ }
361
+ frame = null;
362
+ if (typeof app.frame === "function") {
363
+ try { frame = parse(await app.frame()); } catch { frame = null; }
364
+ if (mine !== refreshToken) return;
365
+ }
366
+ await readView();
367
+ if (mine !== refreshToken) return;
368
+ renderTree(); renderBody(); renderFoot();
369
+ }
370
+
371
+ async function loadDetail(id) {
372
+ detail = null;
373
+ if (typeof app.node !== "function") return;
374
+ try { detail = parse(await app.node(id)); } catch (e) { detail = { error: e.message }; }
375
+ // The selection moved on while this was in flight.
376
+ if (selected !== id) detail = null;
377
+ }
378
+
379
+ // A hit test the app did not provide. Reverse document order over the boxes
380
+ // the tree already carries, deepest-last — an approximation of the paint
381
+ // order, and said to be one: an app that offers `hit` gets the real thing.
382
+ function fallbackHit(x, y) {
383
+ if (!tree) return "";
384
+ for (let i = tree.nodes.length - 1; i >= 0; i--) {
385
+ const n = tree.nodes[i];
386
+ const [bx, by, bw, bh] = n.box;
387
+ if (x >= bx && y >= by && x <= bx + bw && y <= by + bh) return n.id;
388
+ }
389
+ return "";
390
+ }
391
+ // A pointer moves faster than a slow host answers, so the answers can come
392
+ // back out of order. The newest question is the only one whose answer is
393
+ // allowed to move the highlight.
394
+ let hitToken = 0;
395
+
396
+ async function hitAt(x, y) {
397
+ if (typeof app.hit === "function") {
398
+ const mine = ++hitToken;
399
+ try {
400
+ const id = (await app.hit(x, y)) || "";
401
+ if (mine !== hitToken) return null;
402
+ return id;
403
+ } catch { /* fall through to the geometric one */ }
404
+ }
405
+ return fallbackHit(x, y);
406
+ }
407
+
408
+ // --- selection -----------------------------------------------------------
409
+
410
+ async function select(id, { reveal = true, quiet = false } = {}) {
411
+ if (!byId.has(id)) return;
412
+ selected = id;
413
+ await loadDetail(id);
414
+ if (selected !== id) return;
415
+ if (reveal) {
416
+ let p = byId.get(id).p;
417
+ while (p != null) { expanded.add(p); p = byId.get(p) ? byId.get(p).p : null; }
418
+ }
419
+ renderTree(); renderBody(); renderFoot();
420
+ // `quiet` is the opening selection: the panes are worth filling, four
421
+ // coloured rings around the entire page are not.
422
+ if (quiet) overlay.hide(); else highlight(id);
423
+ const row = treeView.querySelector(`[data-id="${cssEscape(id)}"]`);
424
+ if (row && reveal) row.scrollIntoView({ block: "nearest" });
425
+ }
426
+ const cssEscape = (s) => (window.CSS && CSS.escape ? CSS.escape(s) : s.replace(/["\\]/g, "\\$&"));
427
+
428
+ function highlight(id) {
429
+ const n = id ? byId.get(id) : null;
430
+ if (!n) { overlay.hide(); return; }
431
+ overlay.show(n, surfaceRect(), scale(), transform());
432
+ }
433
+
434
+ // --- the tree view -------------------------------------------------------
435
+
436
+ function matches(n) {
437
+ if (!filter) return true;
438
+ const f = filter.toLowerCase();
439
+ if (f[0] === "#") return (n.tid || "").toLowerCase().includes(f.slice(1));
440
+ if (f[0] === ".") return (n.cls || "").toLowerCase().includes(f.slice(1));
441
+ return ((n.tag || "") + " " + (n.tid || "") + " " + (n.cls || "") + " " + (n.text || ""))
442
+ .toLowerCase().includes(f);
443
+ }
444
+
445
+ function renderTree() {
446
+ treeView.textContent = "";
447
+ if (!tree) return;
448
+ // With a filter on, the tree is flattened to the matches: a hierarchy with
449
+ // most of it hidden reads as a broken tree, not as a search result.
450
+ if (filter) {
451
+ const hits = tree.nodes.filter(matches).slice(0, 400);
452
+ for (const n of hits) treeView.appendChild(rowFor(n, 0));
453
+ if (!hits.length) treeView.appendChild(el("div", "evgi-empty", "nothing matches"));
454
+ return;
455
+ }
456
+ const walk = (id, depth) => {
457
+ const n = byId.get(id);
458
+ if (!n) return;
459
+ treeView.appendChild(rowFor(n, depth));
460
+ if (!expanded.has(id)) return;
461
+ for (const k of kids.get(id) || []) walk(k, depth + 1);
462
+ };
463
+ walk(tree.root, 0);
464
+ }
465
+
466
+ function rowFor(n, depth) {
467
+ const row = el("div", "evgi-row" + (n.id === selected ? " sel" : ""));
468
+ row.dataset.id = n.id;
469
+ row.style.paddingLeft = 4 + depth * 11 + "px";
470
+ const kidCount = (kids.get(n.id) || []).length;
471
+ const tw = el("span", "evgi-tw", kidCount ? (expanded.has(n.id) ? "▾" : "▸") : "");
472
+ if (kidCount) {
473
+ tw.onclick = (e) => {
474
+ e.stopPropagation();
475
+ if (expanded.has(n.id)) expanded.delete(n.id); else expanded.add(n.id);
476
+ renderTree();
477
+ };
478
+ }
479
+ row.appendChild(tw);
480
+ row.appendChild(el("span", "evgi-tag", n.tag || "?"));
481
+ if (n.tid) row.appendChild(el("span", "evgi-id", "#" + n.tid));
482
+ if (n.cls) row.appendChild(el("span", "evgi-cls", "." + n.cls.split(/\s+/).join(".")));
483
+ if (n.text) row.appendChild(el("span", "evgi-txt", " " + n.text));
484
+ if (!n.text && kidCount) row.appendChild(el("span", "evgi-dim", ` ${kidCount}`));
485
+ row.onclick = () => select(n.id, { reveal: false });
486
+ row.onmouseenter = () => highlight(n.id);
487
+ row.onmouseleave = () => highlight(selected);
488
+ return row;
489
+ }
490
+
491
+ // --- the detail panes ----------------------------------------------------
492
+
493
+ function renderTabs() {
494
+ for (const [key] of TABS) tabEls[key].classList.toggle("on", key === tab);
495
+ }
496
+
497
+ function kv(rows) {
498
+ const grid = el("div", "evgi-kv");
499
+ for (const [k, v, cls] of rows) {
500
+ grid.appendChild(el("div", "k", k));
501
+ const d = el("div", "v" + (cls ? " " + cls : ""));
502
+ if (/^(#|rgb|hsl)/.test(String(v))) {
503
+ const sw = el("span", "evgi-swatch");
504
+ sw.style.background = v;
505
+ d.append(sw, document.createTextNode(String(v)));
506
+ } else d.textContent = String(v);
507
+ grid.appendChild(d);
508
+ }
509
+ return grid;
510
+ }
511
+
512
+ function renderBody() {
513
+ renderTabs();
514
+ body.textContent = "";
515
+ const n = selected ? byId.get(selected) : null;
516
+ if (!n) {
517
+ body.appendChild(el("div", "evgi-empty",
518
+ tree ? "Pick an element in the picture, or choose one from the tree." : "No tree."));
519
+ return;
520
+ }
521
+ if (tab === "box") renderBox(n);
522
+ else if (tab === "styles") renderStyles(n);
523
+ else if (tab === "css") renderCss();
524
+ else if (tab === "cmds") renderCmds(n);
525
+ else renderRaw(n);
526
+ }
527
+
528
+ function renderBox(n) {
529
+ const [x, y, w, h] = n.box;
530
+ const m = n.m || [0, 0, 0, 0], b = n.b || 0, pd = n.pd || [0, 0, 0, 0];
531
+ const wrap = el("div", "evgi-boxmodel");
532
+ const mb = el("div", "evgi-bm m"); mb.appendChild(el("span", "lbl", "margin"));
533
+ const bb = el("div", "evgi-bm b"); bb.appendChild(el("span", "lbl", "border"));
534
+ const pb = el("div", "evgi-bm p"); pb.appendChild(el("span", "lbl", "padding"));
535
+ const cb = el("div", "evgi-bm c");
536
+ const content = detail && detail.content ? detail.content : [x + b + pd[3], y + b + pd[0],
537
+ w - 2 * b - pd[1] - pd[3], h - 2 * b - pd[0] - pd[2]];
538
+ cb.textContent = `${px(content[2])} × ${px(content[3])}`;
539
+ mb.append(top(m[0]), bb); bb.append(top(b), pb); pb.append(top(pd[0]), cb);
540
+ // The four sides are shown as the browser shows them: the number on each
541
+ // edge of its own ring.
542
+ function top(v) { const d = el("div", "", String(px(v))); d.style.fontSize = "10px"; d.style.opacity = ".8"; return d; }
543
+ wrap.appendChild(mb);
544
+ body.append(el("h4", null, "box model"), wrap);
545
+ body.append(el("h4", null, "geometry"), kv([
546
+ ["position", `${px(x)}, ${px(y)}`],
547
+ ["border box", `${px(w)} × ${px(h)}`],
548
+ ["content box", `${px(content[2])} × ${px(content[3])}`],
549
+ ["margin", m.map(px).join(" ")],
550
+ ["border", String(px(b))],
551
+ ["padding", pd.map(px).join(" ")],
552
+ ]));
553
+ const idRows = [["path", n.id], ["tag", n.tag || ""]];
554
+ if (n.tid) idRows.push(["id", n.tid]);
555
+ if (n.cls) idRows.push(["class", n.cls]);
556
+ if (n.role) idRows.push(["role", n.role]);
557
+ if (n.flags) idRows.push(["flags", n.flags.join(" ")]);
558
+ idRows.push(["children", String(n.kids ?? 0)]);
559
+ if (detail && detail.styleSlot != null && detail.styleSlot >= 0) {
560
+ idRows.push(["style plan", "#" + detail.styleSlot]);
561
+ }
562
+ body.append(el("h4", null, "identity"), kv(idRows));
563
+ if (n.text) body.append(el("h4", null, "text"), kv([["content", detail?.textFull || n.text]]));
564
+ }
565
+
566
+ function renderStyles(n) {
567
+ if (!detail) {
568
+ body.appendChild(el("div", "evgi-empty",
569
+ "This host does not expose node() — only the tree is available."));
570
+ return;
571
+ }
572
+ if (detail.error) { body.appendChild(el("div", "evgi-empty", detail.error)); return; }
573
+ const inline = new Set(detail.inline || []);
574
+
575
+ // --- states held on ------------------------------------------------------
576
+ //
577
+ // A `:hover` rule cannot be read while the only way to make it true is to
578
+ // keep the pointer somewhere other than this panel. These hold the four
579
+ // interaction bits on for the selected node until they are let go, which
580
+ // is the same thing Chrome's `:hov` does and for the same reason.
581
+ if (typeof app.force === "function") {
582
+ const bits = detail.forced || 0;
583
+ const row = el("div", "evgi-hov");
584
+ for (const [bit, label] of [[1, ":hover"], [2, ":focus"], [4, ":active"], [8, ":disabled"]]) {
585
+ const b = el("button", (bits & bit) ? "on" : "", label);
586
+ b.title = (bits & bit) ? "held on — click to let go" : "hold this state on";
587
+ b.onclick = async () => {
588
+ await app.force(n.id, bits ^ bit);
589
+ await refreshTree();
590
+ await select(n.id, { reveal: false });
591
+ };
592
+ row.appendChild(b);
593
+ }
594
+ if (bits) {
595
+ const rel = el("button", "rel", "let go");
596
+ rel.onclick = async () => {
597
+ await app.force(n.id, 0);
598
+ await refreshTree();
599
+ await select(n.id, { reveal: false });
600
+ };
601
+ row.appendChild(rel);
602
+ }
603
+ body.append(el("h4", null, "state"), row);
604
+ if (bits) {
605
+ body.appendChild(el("div", "evgi-note",
606
+ "Held on. The cascade sees an ordinary state — nothing was taught to it — "
607
+ + "so the rules below are the ones that are really matching."));
608
+ }
609
+ }
610
+
611
+ // --- which classes reach this element ------------------------------------
612
+ //
613
+ // This cascade selects on classes and nothing else, so the set of rules
614
+ // that can ever touch a node is exactly the set written against one of its
615
+ // classes. That makes the list short, exact, and worth showing first: it
616
+ // is the answer to "where do I go to change this".
617
+ const classes = detail.classes || [];
618
+ if (classes.length) {
619
+ body.append(el("h4", null, "classes on this element"));
620
+ const wrap = el("div");
621
+ for (const c of classes) {
622
+ const chip = el("span", "evgi-chip");
623
+ chip.append(el("b", null, "." + c.name), el("span", null, `${c.matches} element${c.matches === 1 ? "" : "s"}`));
624
+ chip.title = `.${c.name} matches ${c.matches} element${c.matches === 1 ? "" : "s"} in this tree`;
625
+ chip.onclick = () => { filter = "." + c.name; search.value = filter; renderTree(); };
626
+ wrap.appendChild(chip);
627
+ }
628
+ body.appendChild(wrap);
629
+ const many = classes.filter((c) => c.matches > 1);
630
+ if (many.length) {
631
+ body.appendChild(el("div", "evgi-note",
632
+ "Editing one of these changes every element that carries it — there is no "
633
+ + "way to reach one instance without giving it a class of its own."));
634
+ }
635
+ } else if (n.cls === undefined) {
636
+ body.append(el("h4", null, "classes on this element"));
637
+ body.appendChild(el("div", "evgi-note",
638
+ "None. The cascade skips an element with no class, so no stylesheet rule "
639
+ + "can reach this one at all — whatever it looks like was set on it directly."));
640
+ }
641
+
642
+ // --- the rules, grouped, winners and losers ------------------------------
643
+ const cascade = detail.cascade || [];
644
+ if (cascade.length) {
645
+ body.append(el("h4", null, "rules, strongest last"));
646
+ // Group consecutive declarations that came from the same selector: the
647
+ // plan is flat, but a person reads rules.
648
+ const groups = [];
649
+ for (const d of cascade) {
650
+ const last = groups[groups.length - 1];
651
+ if (last && last.sel === d.sel && last.media === d.media) last.decls.push(d);
652
+ else groups.push({ sel: d.sel, media: d.media, cls: d.cls, decls: [d] });
653
+ }
654
+ const matchOf = new Map(classes.map((c) => [c.name, c.matches]));
655
+ for (const g of groups) {
656
+ const box = el("div", "evgi-rule");
657
+ const head = el("header");
658
+ head.appendChild(el("span", "sel", g.sel));
659
+ if (g.media) head.appendChild(el("span", "media", "@media " + g.media));
660
+ const m = matchOf.get(g.cls);
661
+ if (m !== undefined) head.appendChild(el("span", "cnt", `${m} element${m === 1 ? "" : "s"}`));
662
+ box.appendChild(head);
663
+ const decls = el("div", "decls");
664
+ for (const d of g.decls) {
665
+ const cls = d.beatenByInline ? "inl" : (d.win ? "" : "lost");
666
+ const k = el("div", "k" + (cls ? " " + cls : ""), d.p);
667
+ const v = el("div", "v" + (cls ? " " + cls : ""));
668
+ if (/^(#|rgb|hsl)/.test(String(d.v))) {
669
+ const sw = el("span", "evgi-swatch");
670
+ sw.style.background = d.v;
671
+ v.append(sw, document.createTextNode(d.v));
672
+ } else v.textContent = d.v;
673
+ if (d.beatenByInline) v.title = "the app sets this on the element; no rule can win";
674
+ else if (!d.win) v.title = "overridden by a later rule";
675
+ decls.append(k, v);
676
+ }
677
+ box.appendChild(decls);
678
+ body.appendChild(box);
679
+ }
680
+ if (cascade.some((d) => d.beatenByInline)) {
681
+ body.appendChild(el("div", "evgi-note",
682
+ "A struck line marked as inline is not lost to another rule — the app "
683
+ + "writes that property onto the element itself, and an inline value "
684
+ + "outranks every rule. Editing the sheet cannot change it."));
685
+ }
686
+ } else {
687
+ body.append(el("h4", null, "rules"));
688
+ body.appendChild(el("div", "evgi-note",
689
+ detail.classes
690
+ ? "No rule in the sheet writes to this element."
691
+ : "This host did not hand the inspector its stylesheet, so what SET each "
692
+ + "value cannot be shown — only what the value is."));
693
+ }
694
+
695
+ // --- and the values that came out ----------------------------------------
696
+ const rows = Object.entries(detail.computed || {})
697
+ .map(([k, v]) => [k, v, inline.has(k) ? "ov" : ""]);
698
+ body.append(el("h4", null, `computed · ${rows.length} properties`), kv(rows));
699
+ if (inline.size) {
700
+ body.append(el("h4", null, "set on the element, not by a rule"),
701
+ el("div", "evgi-cmd", [...inline].join(", ")));
702
+ body.appendChild(el("div", "evgi-note",
703
+ "Highlighted above. This is the cascade's own record of what the authoring "
704
+ + "layer set directly — it is what the cascade consults to know what it must "
705
+ + "not overwrite, so it is read here rather than guessed."));
706
+ }
707
+ }
708
+
709
+ // --- the stylesheet, as the input it is ------------------------------------
710
+ //
711
+ // The element tree is the app's output; the sheet is its input. So this pane
712
+ // edits a real input and hands it back, and the app re-parses and
713
+ // re-cascades exactly as it did at startup. Nothing is intercepted, no value
714
+ // is held over the app's head, and the text in this box is the text that
715
+ // goes in the file — there is no "copy as CSS" step because there is nothing
716
+ // to translate.
717
+ let cssDraft = null;
718
+ async function renderCss() {
719
+ if (typeof app.css !== "function") {
720
+ body.appendChild(el("div", "evgi-empty",
721
+ "This host does not expose its stylesheet. A host that hands the panel "
722
+ + "css() gets a live editor here; the tree stays readable either way."));
723
+ return;
724
+ }
725
+ // Awaited, so the pane is drawn into the body it belongs to and not into
726
+ // whichever one is showing by the time the answer lands.
727
+ const forTab = tab;
728
+ const holder = el("div");
729
+ body.appendChild(holder);
730
+ let src;
731
+ try { src = await app.css(); } catch (e) { holder.appendChild(el("div", "evgi-empty", String(e.message))); return; }
732
+ if (tab !== forTab) return;
733
+ holder.textContent = "";
734
+ const wrap = el("div", "evgi-css");
735
+ const bar = el("div", "bar");
736
+ bar.appendChild(el("span", "name", src.name || "stylesheet"));
737
+ const apply = el("button", "evgi-btn", "apply");
738
+ const save = el("button", "evgi-btn", "save to disk");
739
+ const revert = el("button", "evgi-btn", "revert");
740
+ bar.append(apply);
741
+ if (typeof app.saveCss === "function") bar.append(save);
742
+ bar.append(revert);
743
+ if (src.href) bar.appendChild(el("span", "live", "live from disk"));
744
+ wrap.appendChild(bar);
745
+
746
+ const area = el("textarea");
747
+ area.spellcheck = false;
748
+ area.value = cssDraft != null ? cssDraft : (src.text || "");
749
+ area.oninput = () => { cssDraft = area.value; };
750
+ // Tab indents rather than leaving the box: this is an editor, and losing
751
+ // your place to focus the next button is not what the key is for here.
752
+ area.onkeydown = (ev) => {
753
+ if (ev.key === "Tab") {
754
+ ev.preventDefault();
755
+ const a0 = area.selectionStart, b0 = area.selectionEnd;
756
+ area.value = area.value.slice(0, a0) + " " + area.value.slice(b0);
757
+ area.selectionStart = area.selectionEnd = a0 + 2;
758
+ cssDraft = area.value;
759
+ }
760
+ if ((ev.metaKey || ev.ctrlKey) && ev.key === "Enter") { ev.preventDefault(); apply.onclick(); }
761
+ };
762
+ wrap.appendChild(area);
763
+
764
+ const errs = el("div", "err");
765
+ const showErrors = (list) => {
766
+ errs.textContent = "";
767
+ for (const e of list || []) errs.appendChild(el("div", null, e));
768
+ };
769
+ showErrors(src.errors);
770
+ wrap.appendChild(errs);
771
+ wrap.appendChild(el("div", "evgi-note",
772
+ "Applying re-parses the sheet and re-cascades the tree — the same path the "
773
+ + "app took at startup. Saving the file on disk does the same thing by the "
774
+ + "other route, and the two cannot drift."));
775
+
776
+ apply.onclick = async () => {
777
+ if (typeof app.setCss !== "function") return;
778
+ apply.disabled = true;
779
+ try {
780
+ await app.setCss(area.value);
781
+ await refreshTree();
782
+ tab = "css";
783
+ renderTabs();
784
+ renderBody();
785
+ } catch (e) { showErrors([String(e.message)]); apply.disabled = false; }
786
+ };
787
+ // Apply, then write it where it came from. Applying first means a sheet
788
+ // that will not parse is found before it is on disk, and the two together
789
+ // are what closes the loop the other way: the watch sees the write and
790
+ // every other page open on this sheet re-cascades too.
791
+ save.onclick = async () => {
792
+ save.disabled = true;
793
+ try {
794
+ await app.setCss(area.value);
795
+ await app.saveCss(area.value);
796
+ cssDraft = null;
797
+ await refreshTree();
798
+ renderBody();
799
+ } catch (e) { showErrors([String(e.message)]); save.disabled = false; }
800
+ };
801
+ revert.onclick = () => { cssDraft = null; renderBody(); };
802
+ holder.appendChild(wrap);
803
+ }
804
+
805
+ // Which commands this element emitted. Present only when the host built the
806
+ // list with attribution on; without it the answer would be a guess made from
807
+ // rectangles, and a guess is worse than a blank pane.
808
+ function renderCmds(n) {
809
+ if (!frame) {
810
+ body.appendChild(el("div", "evgi-empty",
811
+ "This host does not expose frame() — no display list to attribute."));
812
+ return;
813
+ }
814
+ const list = frame.list ? frame.list.cmds : frame.cmds;
815
+ if (!list) { body.appendChild(el("div", "evgi-empty", "no commands in the frame")); return; }
816
+ if (list.length && list[0].n === undefined) {
817
+ body.appendChild(el("div", "evgi-empty",
818
+ "The frame carries no attribution. Set `attribute` on the display list "
819
+ + "before building it, and the commands each element emitted show up here."));
820
+ return;
821
+ }
822
+ const KIND = ["RECT", "BORDER", "IMAGE", "TEXT", "PUSH_CLIP", "POP_CLIP", "PATH", "STROKE"];
823
+ const mine = [];
824
+ list.forEach((c, i) => { if (c.n === n.slot) mine.push([i, c]); });
825
+ body.append(el("h4", null, `${mine.length} of ${list.length} commands`));
826
+ if (!mine.length) {
827
+ body.appendChild(el("div", "evgi-empty",
828
+ "This element drew nothing. A layout box with no background is normal."));
829
+ return;
830
+ }
831
+ for (const [i, c] of mine.slice(0, 200)) {
832
+ const row = el("div", "evgi-cmd");
833
+ row.append(el("span", "evgi-dim", String(i).padStart(4, " ") + " "));
834
+ row.appendChild(el("b", null, KIND[c.k] || String(c.k)));
835
+ const bits = [` ${px(c.x)},${px(c.y)}`];
836
+ if (c.w || c.h) bits.push(`${px(c.w)}×${px(c.h)}`);
837
+ if (c.r) bits.push(`r${px(c.r)}`);
838
+ if (c.text != null) bits.push(JSON.stringify(c.text));
839
+ if (c.font) bits.push(c.font + " " + px(c.size || 0));
840
+ row.append(document.createTextNode(" " + bits.join(" ")));
841
+ if (c.c) {
842
+ const sw = el("span", "evgi-swatch");
843
+ sw.style.background = `rgba(${c.c[0]},${c.c[1]},${c.c[2]},${c.c[3]})`;
844
+ row.append(document.createTextNode(" "), sw);
845
+ }
846
+ body.appendChild(row);
847
+ }
848
+ }
849
+
850
+ function renderRaw(n) {
851
+ const pre = el("pre");
852
+ pre.style.cssText = "white-space:pre-wrap;font:11px ui-monospace,Menlo,monospace;margin:0";
853
+ pre.textContent = JSON.stringify({ node: n, detail }, null, 1);
854
+ body.appendChild(pre);
855
+ }
856
+
857
+ function renderFoot() {
858
+ foot.textContent = "";
859
+ if (!tree) { foot.append("no tree"); return; }
860
+ foot.append(`${tree.nodes.length} nodes`);
861
+ if (frame) {
862
+ const list = frame.list ? frame.list.cmds : frame.cmds;
863
+ if (list) foot.append(` · ${list.length} commands`);
864
+ }
865
+ foot.append(` · ${px(tree.w)}×${px(tree.h)}`);
866
+ if (foot.dataset.note) {
867
+ const n = el("span", "", " · " + foot.dataset.note);
868
+ n.style.color = "#f2b661";
869
+ foot.append(n);
870
+ }
871
+ }
872
+
873
+ // --- picking -------------------------------------------------------------
874
+
875
+ // A pointer lands in CSS pixels; the app answers in the coordinates its own
876
+ // tree is in. Both conversions happen here so an adapter never has to know
877
+ // where on the page its picture ended up.
878
+ function surfacePoint(ev) {
879
+ const r = surfaceRect();
880
+ const s = scale() || 1;
881
+ const t = transform();
882
+ const sx = (ev.clientX - r.left) / s;
883
+ const sy = (ev.clientY - r.top) / s;
884
+ return [(sx - t.x) / (t.k || 1), (sy - t.y) / (t.k || 1)];
885
+ }
886
+ function setPicking(on) {
887
+ picking = on;
888
+ pick.classList.toggle("on", on);
889
+ document.body.classList.toggle("evgi-picking", on);
890
+ if (!on) highlight(selected);
891
+ }
892
+ surface.addEventListener("mousemove", async (ev) => {
893
+ if (!picking) return;
894
+ const [x, y] = surfacePoint(ev);
895
+ const id = await hitAt(x, y);
896
+ if (id && picking && byId.has(id)) highlight(id);
897
+ });
898
+ surface.addEventListener("click", async (ev) => {
899
+ if (!picking) return;
900
+ ev.preventDefault(); ev.stopPropagation();
901
+ const [x, y] = surfacePoint(ev);
902
+ const id = await hitAt(x, y);
903
+ if (id && byId.has(id)) { setPicking(false); await select(id); }
904
+ }, true);
905
+ window.addEventListener("keydown", (ev) => {
906
+ if (ev.key === "Escape" && picking) { setPicking(false); ev.preventDefault(); }
907
+ });
908
+
909
+ pick.onclick = () => setPicking(!picking);
910
+ reload.onclick = () => refreshTree();
911
+ close.onclick = () => api.detach();
912
+ search.oninput = () => { filter = search.value.trim(); renderTree(); };
913
+
914
+ window.addEventListener("resize", () => highlight(selected));
915
+
916
+ renderTabs();
917
+ // Open on the root rather than on an empty pane. There is always something
918
+ // to say about the page as a whole, and a panel whose right half is a
919
+ // sentence asking you to click something is a panel that has not started.
920
+ const ready = refreshTree(false).then(() => {
921
+ if (tree && tree.nodes.length) return select(tree.root, { quiet: true });
922
+ });
923
+
924
+ const api = {
925
+ // Resolves when the first tree has been read and the root selected — a
926
+ // caller that wants to drive the panel has to know when it is up.
927
+ ready,
928
+ // A host that repaints tells the panel so; nothing here polls.
929
+ refresh: () => refreshTree(true),
930
+ select,
931
+ get selection() { return selected; },
932
+ detach() {
933
+ setPicking(false);
934
+ overlay.host.remove();
935
+ panel.remove();
936
+ },
937
+ };
938
+ return api;
939
+ }
940
+
941
+ // One line in a host: `?inspect=1` turns it on and nothing else changes.
942
+ export function attachIfRequested(opts = {}) {
943
+ const q = new URLSearchParams(location.search);
944
+ const want = q.get("inspect");
945
+ if (want === null || want === "0" || want === "false") return null;
946
+ return attach(opts);
947
+ }