@sleep2agi/agent-network-dashboard 0.4.6-preview.33 → 0.4.6-preview.331

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 (496) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/diagnostics/route-bundle-stats.json +41 -41
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/prerender-manifest.json +3 -3
  6. package/.next/server/app/_global-error.html +1 -1
  7. package/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +2 -2
  15. package/.next/server/app/_not-found.rsc +12 -12
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  21. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/admin.html +2 -2
  24. package/.next/server/app/admin.rsc +14 -14
  25. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  26. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  27. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  28. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  29. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  30. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  31. package/.next/server/app/index.html +2 -2
  32. package/.next/server/app/index.rsc +14 -14
  33. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  34. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  35. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  36. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  37. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  38. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  39. package/.next/server/app/login.html +2 -2
  40. package/.next/server/app/login.rsc +14 -14
  41. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  42. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  43. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  44. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  46. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  47. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  48. package/.next/server/app/logs.html +2 -2
  49. package/.next/server/app/logs.rsc +14 -14
  50. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  51. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  52. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  53. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  54. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  55. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  56. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  57. package/.next/server/app/messages.html +2 -2
  58. package/.next/server/app/messages.rsc +14 -14
  59. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  60. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  61. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  62. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  64. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  65. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  66. package/.next/server/app/node.html +2 -2
  67. package/.next/server/app/node.rsc +14 -14
  68. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  69. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  70. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  71. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  72. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  73. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  74. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/nodes.html +2 -2
  76. package/.next/server/app/nodes.rsc +14 -14
  77. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  78. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  79. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  80. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  81. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  82. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  83. package/.next/server/app/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/server-logs.html +2 -2
  86. package/.next/server/app/server-logs.rsc +14 -14
  87. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  88. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  89. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  90. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  91. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  92. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  93. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  94. package/.next/server/app/settings/networks.html +2 -2
  95. package/.next/server/app/settings/networks.rsc +14 -14
  96. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  97. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  98. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  99. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  100. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  101. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  102. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  103. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  105. package/.next/server/app/settings/tokens.html +2 -2
  106. package/.next/server/app/settings/tokens.rsc +14 -14
  107. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  108. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  109. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  110. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  111. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  112. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  113. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  114. package/.next/server/app/settings.html +2 -2
  115. package/.next/server/app/settings.rsc +14 -14
  116. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  117. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  118. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  119. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  120. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  121. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  122. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  123. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/tasks.html +2 -2
  125. package/.next/server/app/tasks.rsc +14 -14
  126. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  127. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  128. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  129. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  130. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  131. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  132. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js +1 -1
  133. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js.map +1 -1
  134. package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js +1 -1
  135. package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js.map +1 -1
  136. package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js +1 -1
  137. package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js.map +1 -1
  138. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +4 -1
  139. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  140. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  141. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  142. package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js +1 -1
  143. package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js.map +1 -1
  144. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  145. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  146. package/.next/server/chunks/ssr/agent-network-dashboard_app_0_d45-d._.js +1 -1
  147. package/.next/server/chunks/ssr/agent-network-dashboard_app_0_d45-d._.js.map +1 -1
  148. package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js +1 -1
  149. package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js.map +1 -1
  150. package/.next/server/chunks/ssr/agent-network-dashboard_app_0xgney8._.js +1 -1
  151. package/.next/server/chunks/ssr/agent-network-dashboard_app_0xgney8._.js.map +1 -1
  152. package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js +1 -1
  153. package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js.map +1 -1
  154. package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js +1 -1
  155. package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js.map +1 -1
  156. package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js +1 -1
  157. package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js.map +1 -1
  158. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  159. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  160. package/.next/server/middleware-build-manifest.js +3 -3
  161. package/.next/server/pages/404.html +2 -2
  162. package/.next/server/pages/500.html +1 -1
  163. package/.next/server/server-reference-manifest.js +1 -1
  164. package/.next/server/server-reference-manifest.json +1 -1
  165. package/.next/static/chunks/0-mpa_947ipeq.js +1 -0
  166. package/.next/static/chunks/01lmbsd37fybu.js +1 -0
  167. package/.next/static/chunks/03o.h6kvmw4l_.js +1 -0
  168. package/.next/static/chunks/03~~oirxz7~vc.js +1 -0
  169. package/.next/static/chunks/05uk96gc~9mni.js +1 -0
  170. package/.next/static/chunks/07lsi~s-c0wvh.css +2 -0
  171. package/.next/static/chunks/08kf-.bgs10ze.js +1 -0
  172. package/.next/static/chunks/0ggj5ummw2a84.js +7 -0
  173. package/.next/static/chunks/0glmrdj9112tc.js +7 -0
  174. package/.next/static/chunks/0mpa-fgyi2k78.js +1 -0
  175. package/.next/static/chunks/0nlhul7k4hwil.js +1 -0
  176. package/.next/static/chunks/0rxrez_cjn.eh.js +1 -0
  177. package/.next/static/chunks/0x8x.29hkju5a.js +1 -0
  178. package/.next/static/chunks/174re5ar~kxne.js +1 -0
  179. package/.next/static/chunks/1851sbdemt00t.js +4 -0
  180. package/.next/trace +2 -2
  181. package/.next/trace-build +1 -1
  182. package/app/components/ChatPopover.tsx +38 -2
  183. package/app/components/CommandPalette.tsx +107 -0
  184. package/app/components/HelpOverlay.tsx +111 -1
  185. package/app/components/TopoGraph.tsx +6197 -262
  186. package/app/components/utils.ts +8 -7
  187. package/app/globals.css +110 -5
  188. package/app/lib/time.ts +36 -0
  189. package/app/lib/vendorIdentity.ts +14 -3
  190. package/package.json +2 -2
  191. package/public/sleep2agi-logo.svg +11 -0
  192. package/public/vendors/claude.svg +11 -0
  193. package/public/vendors/minimax.svg +11 -0
  194. package/public/vendors/openai.svg +12 -0
  195. package/screenshots/r293-p0/after-full.png +0 -0
  196. package/screenshots/r293-p0/after-topo.png +0 -0
  197. package/screenshots/r293-p0/baseline-full.png +0 -0
  198. package/screenshots/r293-p0/baseline-topo.png +0 -0
  199. package/screenshots/r293-p0/logo-integrated-full.png +0 -0
  200. package/screenshots/r293-p0/logo-integrated-topo.png +0 -0
  201. package/screenshots/r293-p0/v0.10.0-hero3-rfc.md +230 -0
  202. package/scripts/p0-topo-screenshot.mjs +82 -0
  203. package/scripts/topo-active-filter-pill-test.mjs +101 -0
  204. package/scripts/topo-active-links-chip-hover-test.mjs +103 -0
  205. package/scripts/topo-active-links-chip-nav-test.mjs +117 -0
  206. package/scripts/topo-active-links-chip-self-hover-test.mjs +171 -0
  207. package/scripts/topo-active-links-chip-test.mjs +73 -0
  208. package/scripts/topo-active-links-empty-recede-test.mjs +125 -0
  209. package/scripts/topo-active-links-freshness-test.mjs +113 -0
  210. package/scripts/topo-active-links-tooltip-test.mjs +79 -0
  211. package/scripts/topo-active-pulse-ease-test.mjs +120 -0
  212. package/scripts/topo-auto-fit-test.mjs +73 -0
  213. package/scripts/topo-brand-logo-transition-test.mjs +94 -0
  214. package/scripts/topo-brand-watermark-test.mjs +97 -0
  215. package/scripts/topo-canvas-aria-test.mjs +108 -0
  216. package/scripts/topo-chat-target-breath-test.mjs +84 -0
  217. package/scripts/topo-chat-target-ring-ease-test.mjs +82 -0
  218. package/scripts/topo-chat-target-ring-fade-test.mjs +124 -0
  219. package/scripts/topo-chip-empty-recede-test.mjs +116 -0
  220. package/scripts/topo-chip-focus-test.mjs +95 -0
  221. package/scripts/topo-chip-pin-mirror-test.mjs +113 -0
  222. package/scripts/topo-chip-row-semantic-gap-test.mjs +97 -0
  223. package/scripts/topo-chip-row-tabular-test.mjs +111 -0
  224. package/scripts/topo-chiprow-data-weight-test.mjs +94 -0
  225. package/scripts/topo-chrome-a11y-test.mjs +107 -0
  226. package/scripts/topo-chrome-focus-ring-unify-test.mjs +100 -0
  227. package/scripts/topo-chrome-icon-stroke-test.mjs +95 -0
  228. package/scripts/topo-chrome-inactive-color-test.mjs +90 -0
  229. package/scripts/topo-chrome-pop-extension-test.mjs +98 -0
  230. package/scripts/topo-chrome-pop-nodesize-test.mjs +88 -0
  231. package/scripts/topo-chrome-press-state-test.mjs +135 -0
  232. package/scripts/topo-chrome-strip-semantic-gap-test.mjs +99 -0
  233. package/scripts/topo-chrome-strip-theme-ease-test.mjs +99 -0
  234. package/scripts/topo-chrome-zoom-level-pop-test.mjs +122 -0
  235. package/scripts/topo-chrome-zoom-pop-test.mjs +130 -0
  236. package/scripts/topo-chrome-zoom-readout-weight-test.mjs +81 -0
  237. package/scripts/topo-click-ripple-ease-test.mjs +94 -0
  238. package/scripts/topo-cmdk-edge-clear-test.mjs +109 -0
  239. package/scripts/topo-cmdk-layout-view-test.mjs +108 -0
  240. package/scripts/topo-cmdk-pin-test.mjs +123 -0
  241. package/scripts/topo-cmdk-vendor-clear-test.mjs +133 -0
  242. package/scripts/topo-cmdk-vendor-pin-test.mjs +103 -0
  243. package/scripts/topo-corner-radius-unify-test.mjs +103 -0
  244. package/scripts/topo-cwd-tooltip-test.mjs +64 -0
  245. package/scripts/topo-dblclick-reset-test.mjs +74 -0
  246. package/scripts/topo-dense-alias-fill-transition-test.mjs +133 -0
  247. package/scripts/topo-discrete-zoom-smooth-test.mjs +125 -0
  248. package/scripts/topo-edge-badge-click-pin-test.mjs +94 -0
  249. package/scripts/topo-edge-badge-crossfade-test.mjs +138 -0
  250. package/scripts/topo-edge-badge-hot-test.mjs +115 -0
  251. package/scripts/topo-edge-badge-hover-test.mjs +85 -0
  252. package/scripts/topo-edge-badge-lift-test.mjs +150 -0
  253. package/scripts/topo-edge-badge-ripple-test.mjs +111 -0
  254. package/scripts/topo-edge-badge-stroke-transition-test.mjs +118 -0
  255. package/scripts/topo-edge-badge-text-pin-test.mjs +121 -0
  256. package/scripts/topo-edge-badge-theme-ease-test.mjs +89 -0
  257. package/scripts/topo-edge-count-badge-test.mjs +86 -0
  258. package/scripts/topo-edge-endpoint-highlight-test.mjs +111 -0
  259. package/scripts/topo-edge-fadein-test.mjs +106 -0
  260. package/scripts/topo-edge-focus-test.mjs +113 -0
  261. package/scripts/topo-edge-hover-test.mjs +93 -0
  262. package/scripts/topo-edge-pill-hot-test.mjs +129 -0
  263. package/scripts/topo-edge-pill-test.mjs +105 -0
  264. package/scripts/topo-edge-ping-pulse-retired-test.mjs +119 -0
  265. package/scripts/topo-edge-stroke-ease-test.mjs +100 -0
  266. package/scripts/topo-edge-stroke-transition-test.mjs +132 -0
  267. package/scripts/topo-edge-tooltip-test.mjs +70 -0
  268. package/scripts/topo-empty-hint-tracking-test.mjs +84 -0
  269. package/scripts/topo-empty-state-breath-test.mjs +150 -0
  270. package/scripts/topo-empty-state-crossfade-test.mjs +130 -0
  271. package/scripts/topo-empty-state-recenter-test.mjs +107 -0
  272. package/scripts/topo-empty-subhint-tracking-test.mjs +87 -0
  273. package/scripts/topo-endpoint-ring-fade-test.mjs +132 -0
  274. package/scripts/topo-endpoint-ring-test.mjs +93 -0
  275. package/scripts/topo-endpoint-ring-thicken-test.mjs +112 -0
  276. package/scripts/topo-esc-clears-pin-test.mjs +98 -0
  277. package/scripts/topo-filter-pill-aliases-test.mjs +121 -0
  278. package/scripts/topo-filter-pill-body-click-test.mjs +112 -0
  279. package/scripts/topo-filter-pill-count-test.mjs +110 -0
  280. package/scripts/topo-filter-pill-mobile-test.mjs +90 -0
  281. package/scripts/topo-fit-key-test.mjs +106 -0
  282. package/scripts/topo-freshness-chip-test.mjs +74 -0
  283. package/scripts/topo-freshness-chip-transition-test.mjs +80 -0
  284. package/scripts/topo-freshness-chip-weight-test.mjs +90 -0
  285. package/scripts/topo-freshness-conditional-render-test.mjs +92 -0
  286. package/scripts/topo-fullscreen-active-test.mjs +115 -0
  287. package/scripts/topo-group-box-pinned-state-test.mjs +95 -0
  288. package/scripts/topo-group-count-test.mjs +6 -1
  289. package/scripts/topo-group-label-a11y-test.mjs +113 -0
  290. package/scripts/topo-group-label-count-inherit-test.mjs +99 -0
  291. package/scripts/topo-group-label-hover-test.mjs +119 -0
  292. package/scripts/topo-group-label-pin-spacing-test.mjs +136 -0
  293. package/scripts/topo-group-label-tint-test.mjs +99 -0
  294. package/scripts/topo-group-label-title-test.mjs +83 -0
  295. package/scripts/topo-group-pin-test.mjs +119 -0
  296. package/scripts/topo-group-pip-fadein-test.mjs +100 -0
  297. package/scripts/topo-group-pip-tabular-test.mjs +95 -0
  298. package/scripts/topo-group-status-pips-test.mjs +103 -0
  299. package/scripts/topo-groupbox-fadein-test.mjs +95 -0
  300. package/scripts/topo-groupbox-hover-lift-test.mjs +127 -0
  301. package/scripts/topo-groupbox-hubcore-ease-test.mjs +106 -0
  302. package/scripts/topo-groupbox-live-test.mjs +100 -0
  303. package/scripts/topo-groupbox-march-busyness-test.mjs +155 -0
  304. package/scripts/topo-groupbox-test.mjs +4 -2
  305. package/scripts/topo-help-overlay-r109-test.mjs +67 -0
  306. package/scripts/topo-help-overlay-r118-test.mjs +69 -0
  307. package/scripts/topo-help-overlay-r123-test.mjs +69 -0
  308. package/scripts/topo-help-overlay-r134-test.mjs +74 -0
  309. package/scripts/topo-help-overlay-r137-test.mjs +81 -0
  310. package/scripts/topo-help-overlay-r141-test.mjs +86 -0
  311. package/scripts/topo-help-overlay-r153-test.mjs +85 -0
  312. package/scripts/topo-help-overlay-r59-test.mjs +67 -0
  313. package/scripts/topo-help-overlay-r65-test.mjs +64 -0
  314. package/scripts/topo-help-overlay-r70-test.mjs +63 -0
  315. package/scripts/topo-help-overlay-r78-test.mjs +59 -0
  316. package/scripts/topo-help-overlay-r81-test.mjs +58 -0
  317. package/scripts/topo-help-overlay-r87-test.mjs +64 -0
  318. package/scripts/topo-help-overlay-r91-test.mjs +65 -0
  319. package/scripts/topo-hot-tail-crossfade-test.mjs +117 -0
  320. package/scripts/topo-html-overlay-padding-unify-test.mjs +120 -0
  321. package/scripts/topo-hub-a11y-test.mjs +112 -0
  322. package/scripts/topo-hub-busyness-test.mjs +100 -0
  323. package/scripts/topo-hub-center-crossfade-test.mjs +124 -0
  324. package/scripts/topo-hub-click-fit-test.mjs +137 -0
  325. package/scripts/topo-hub-digit-scale-test.mjs +129 -0
  326. package/scripts/topo-hub-fadein-test.mjs +88 -0
  327. package/scripts/topo-hub-hover-lift-test.mjs +101 -0
  328. package/scripts/topo-hub-hover-ring-test.mjs +82 -0
  329. package/scripts/topo-hub-pip-theme-ease-test.mjs +116 -0
  330. package/scripts/topo-hub-spoke-ease-test.mjs +120 -0
  331. package/scripts/topo-hub-title-test.mjs +93 -0
  332. package/scripts/topo-hub-working-count-test.mjs +99 -0
  333. package/scripts/topo-ingroup-edge-test.mjs +103 -0
  334. package/scripts/topo-inner-radar-ring-retired-test.mjs +89 -0
  335. package/scripts/topo-kicker-color-test.mjs +93 -0
  336. package/scripts/topo-kicker-tracking-test.mjs +101 -0
  337. package/scripts/topo-kicker-weight-test.mjs +98 -0
  338. package/scripts/topo-label-card-theme-ease-test.mjs +85 -0
  339. package/scripts/topo-label-lift-test.mjs +119 -0
  340. package/scripts/topo-lastseen-tooltip-test.mjs +59 -0
  341. package/scripts/topo-layout-hover-text-preview-test.mjs +101 -0
  342. package/scripts/topo-layout-key-test.mjs +91 -0
  343. package/scripts/topo-layout-switch-crossfade-test.mjs +126 -0
  344. package/scripts/topo-layout-toggle-border-unify-test.mjs +104 -0
  345. package/scripts/topo-layout-toggle-test.mjs +131 -0
  346. package/scripts/topo-legend-compress-test.mjs +107 -0
  347. package/scripts/topo-legend-count-empty-test.mjs +126 -0
  348. package/scripts/topo-legend-count-tabular-parity-test.mjs +90 -0
  349. package/scripts/topo-legend-count-tabular-test.mjs +87 -0
  350. package/scripts/topo-legend-count-test.mjs +88 -0
  351. package/scripts/topo-legend-count-tier-fill-test.mjs +121 -0
  352. package/scripts/topo-legend-count-weight-test.mjs +86 -0
  353. package/scripts/topo-legend-header-symmetric-inset-test.mjs +112 -0
  354. package/scripts/topo-legend-hitbox-swatch-center-test.mjs +93 -0
  355. package/scripts/topo-legend-hover-test.mjs +113 -0
  356. package/scripts/topo-legend-offline-delimiter-test.mjs +84 -0
  357. package/scripts/topo-legend-offline-label-simplify-test.mjs +86 -0
  358. package/scripts/topo-legend-panel-count-weight-test.mjs +89 -0
  359. package/scripts/topo-legend-panel-header-test.mjs +83 -0
  360. package/scripts/topo-legend-pin-ring-fade-test.mjs +112 -0
  361. package/scripts/topo-legend-pin-test.mjs +128 -0
  362. package/scripts/topo-legend-row-hover-lift-test.mjs +109 -0
  363. package/scripts/topo-legend-row-labels-simplify-test.mjs +80 -0
  364. package/scripts/topo-legend-row-pin-spacing-test.mjs +124 -0
  365. package/scripts/topo-legend-row-tint-test.mjs +95 -0
  366. package/scripts/topo-legend-row-title-test.mjs +102 -0
  367. package/scripts/topo-legend-swatch-grow-test.mjs +116 -0
  368. package/scripts/topo-legend-swatch-radius-test.mjs +78 -0
  369. package/scripts/topo-minimap-a11y-test.mjs +101 -0
  370. package/scripts/topo-minimap-dot-transition-test.mjs +113 -0
  371. package/scripts/topo-minimap-rect-stroke-test.mjs +94 -0
  372. package/scripts/topo-minimap-test.mjs +68 -0
  373. package/scripts/topo-minimap-viewport-smooth-test.mjs +124 -0
  374. package/scripts/topo-mobile-chips-test.mjs +103 -0
  375. package/scripts/topo-monogram-font-test.mjs +112 -0
  376. package/scripts/topo-monogram-stroke-test.mjs +101 -0
  377. package/scripts/topo-node-a11y-test.mjs +107 -0
  378. package/scripts/topo-node-alias-chat-pin-test.mjs +95 -0
  379. package/scripts/topo-node-hover-lift-test.mjs +85 -0
  380. package/scripts/topo-node-label-card-elevation-test.mjs +157 -0
  381. package/scripts/topo-node-label-card-stroke-test.mjs +135 -0
  382. package/scripts/topo-node-stroke-transition-test.mjs +103 -0
  383. package/scripts/topo-node-text-fill-transition-test.mjs +119 -0
  384. package/scripts/topo-node-title-peers-test.mjs +107 -0
  385. package/scripts/topo-node-title-test.mjs +101 -0
  386. package/scripts/topo-nodesize-active-hover-test.mjs +109 -0
  387. package/scripts/topo-nodesize-crossfade-test.mjs +141 -0
  388. package/scripts/topo-online-chip-nav-test.mjs +143 -0
  389. package/scripts/topo-orbit-particles-retired-test.mjs +91 -0
  390. package/scripts/topo-overlap-test.mjs +32 -5
  391. package/scripts/topo-panel-elevation-test.mjs +76 -0
  392. package/scripts/topo-panel-fadein-test.mjs +102 -0
  393. package/scripts/topo-panel-hot-tail-fade-test.mjs +132 -0
  394. package/scripts/topo-panel-hover-elevation-test.mjs +118 -0
  395. package/scripts/topo-panel-theme-ease-test.mjs +85 -0
  396. package/scripts/topo-panel-title-fill-ease-test.mjs +91 -0
  397. package/scripts/topo-panel-title-tracking-test.mjs +78 -0
  398. package/scripts/topo-particle-stagger-test.mjs +82 -0
  399. package/scripts/topo-pin-intersection-color-ease-test.mjs +111 -0
  400. package/scripts/topo-pin-intersection-empty-test.mjs +129 -0
  401. package/scripts/topo-pin-intersection-tabular-test.mjs +96 -0
  402. package/scripts/topo-pin-intersection-test.mjs +121 -0
  403. package/scripts/topo-pin-intersection-warning-crossfade-test.mjs +107 -0
  404. package/scripts/topo-pin-mirror-transition-test.mjs +101 -0
  405. package/scripts/topo-pin-persistence-test.mjs +105 -0
  406. package/scripts/topo-popover-meta-test.mjs +78 -0
  407. package/scripts/topo-prepublish-guard-test.mjs +95 -0
  408. package/scripts/topo-pressure-bar-test.mjs +69 -0
  409. package/scripts/topo-pressure-hover-test.mjs +123 -0
  410. package/scripts/topo-pressure-pin-test.mjs +119 -0
  411. package/scripts/topo-pressure-seg-self-hover-test.mjs +126 -0
  412. package/scripts/topo-pressure-seg-tooltip-test.mjs +98 -0
  413. package/scripts/topo-pressure-seg-transition-test.mjs +107 -0
  414. package/scripts/topo-pulse-dot-crossfade-test.mjs +122 -0
  415. package/scripts/topo-pulse-dot-retired-test.mjs +86 -0
  416. package/scripts/topo-recent-panel-count-freshness-test.mjs +109 -0
  417. package/scripts/topo-recent-panel-count-test.mjs +72 -0
  418. package/scripts/topo-recent-panel-count-weight-test.mjs +91 -0
  419. package/scripts/topo-recent-panel-footer-breath-test.mjs +142 -0
  420. package/scripts/topo-recent-panel-hot-header-test.mjs +132 -0
  421. package/scripts/topo-recent-panel-more-crossfade-test.mjs +124 -0
  422. package/scripts/topo-recent-panel-more-cyan-hover-test.mjs +151 -0
  423. package/scripts/topo-recent-panel-more-nav-test.mjs +127 -0
  424. package/scripts/topo-recent-panel-more-test.mjs +106 -0
  425. package/scripts/topo-recent-panel-readability-bump-test.mjs +151 -0
  426. package/scripts/topo-recent-row-count-fill-test.mjs +129 -0
  427. package/scripts/topo-recent-row-fadein-test.mjs +107 -0
  428. package/scripts/topo-recent-row-freshness-test.mjs +102 -0
  429. package/scripts/topo-recent-row-hot-test.mjs +110 -0
  430. package/scripts/topo-recent-row-hover-lift-test.mjs +128 -0
  431. package/scripts/topo-recent-row-hover-test.mjs +122 -0
  432. package/scripts/topo-recent-row-pin-spacing-test.mjs +137 -0
  433. package/scripts/topo-recent-row-pin-test.mjs +126 -0
  434. package/scripts/topo-recent-row-tint-test.mjs +91 -0
  435. package/scripts/topo-recent-row-title-test.mjs +112 -0
  436. package/scripts/topo-recent-row-ts-alpha-test.mjs +100 -0
  437. package/scripts/topo-recent-row-ts-test.mjs +95 -0
  438. package/scripts/topo-recent-row-typography-test.mjs +119 -0
  439. package/scripts/topo-recent-signal-empty-hint-test.mjs +73 -0
  440. package/scripts/topo-recent-signal-empty-test.mjs +63 -0
  441. package/scripts/topo-reduced-motion-test.mjs +77 -0
  442. package/scripts/topo-reset-spin-test.mjs +120 -0
  443. package/scripts/topo-runtime-badge-grow-test.mjs +114 -0
  444. package/scripts/topo-spokes-retired-test.mjs +93 -0
  445. package/scripts/topo-starfield-density-test.mjs +89 -0
  446. package/scripts/topo-status-chip-hover-test.mjs +101 -0
  447. package/scripts/topo-status-chip-tooltip-test.mjs +95 -0
  448. package/scripts/topo-statusbar-chips-self-hover-test.mjs +177 -0
  449. package/scripts/topo-svg-focus-test.mjs +112 -0
  450. package/scripts/topo-sweep-busyness-test.mjs +94 -0
  451. package/scripts/topo-tabular-nums-sweep-test.mjs +93 -0
  452. package/scripts/topo-tier-guide-dashes-test.mjs +86 -0
  453. package/scripts/topo-tier-ring-fadein-test.mjs +107 -0
  454. package/scripts/topo-tier-ring-occupancy-test.mjs +87 -0
  455. package/scripts/topo-tier-ring-test.mjs +69 -0
  456. package/scripts/topo-tier-ring-tint-test.mjs +110 -0
  457. package/scripts/topo-tier-stagger-test.mjs +89 -0
  458. package/scripts/topo-title-block-tight-leading-test.mjs +95 -0
  459. package/scripts/topo-title-tracking-test.mjs +98 -0
  460. package/scripts/topo-titleblock-gap-test.mjs +86 -0
  461. package/scripts/topo-titleblock-margin-test.mjs +83 -0
  462. package/scripts/topo-toggle-inactive-cyan-hover-test.mjs +110 -0
  463. package/scripts/topo-top-rail-theme-ease-test.mjs +89 -0
  464. package/scripts/topo-tz-ghost-test.mjs +69 -0
  465. package/scripts/topo-vendor-chip-hover-test.mjs +111 -0
  466. package/scripts/topo-vendor-chip-self-hover-test.mjs +131 -0
  467. package/scripts/topo-vendor-chip-tabular-test.mjs +92 -0
  468. package/scripts/topo-vendor-chip-threshold-test.mjs +78 -0
  469. package/scripts/topo-vendor-chip-weight-test.mjs +87 -0
  470. package/scripts/topo-vendor-letter-tooltip-test.mjs +92 -0
  471. package/scripts/topo-vendor-pill-test.mjs +125 -0
  472. package/scripts/topo-vendor-pin-test.mjs +125 -0
  473. package/scripts/topo-view-smooth-test.mjs +131 -0
  474. package/scripts/topo-watermark-tracking-test.mjs +99 -0
  475. package/scripts/topo-working-chip-pin-test.mjs +154 -0
  476. package/scripts/topo-working-halo-breath-retired-test.mjs +99 -0
  477. package/scripts/topo-wrapper-flow-arrow-theme-test.mjs +97 -0
  478. package/scripts/topo-wrapper-shadow-theme-ease-test.mjs +89 -0
  479. package/.next/static/chunks/0-7iowzu4.798.js +0 -1
  480. package/.next/static/chunks/02t5jr4shh.hy.js +0 -1
  481. package/.next/static/chunks/09nd.t_hd.y6t.css +0 -2
  482. package/.next/static/chunks/0_8snedeata9k.js +0 -1
  483. package/.next/static/chunks/0_qfg5s5uzs7..js +0 -7
  484. package/.next/static/chunks/0elalb9n_.~di.js +0 -1
  485. package/.next/static/chunks/0j_q__o8p6g83.js +0 -1
  486. package/.next/static/chunks/0jl6_w50zh4l5.js +0 -1
  487. package/.next/static/chunks/0kwf..oq7.27x.js +0 -7
  488. package/.next/static/chunks/0r4q73.-1bkyl.js +0 -1
  489. package/.next/static/chunks/0r_c8eccg3z2o.js +0 -1
  490. package/.next/static/chunks/0sjqjenh7~k-j.js +0 -1
  491. package/.next/static/chunks/0y8v4grez.a76.js +0 -1
  492. package/.next/static/chunks/10oi7l-tycgl9.js +0 -1
  493. package/.next/static/chunks/11uk1izuv3~0t.js +0 -1
  494. /package/.next/static/{Q3_3F7glo-M3CFrXA8POE → 3PMYuA9TBRow9F7QeNZc7}/_buildManifest.js +0 -0
  495. /package/.next/static/{Q3_3F7glo-M3CFrXA8POE → 3PMYuA9TBRow9F7QeNZc7}/_clientMiddlewareManifest.js +0 -0
  496. /package/.next/static/{Q3_3F7glo-M3CFrXA8POE → 3PMYuA9TBRow9F7QeNZc7}/_ssgManifest.js +0 -0
@@ -0,0 +1,126 @@
1
+ /* Round 170 verification: layout toggle (Ring ↔ Grid) crossfade.
2
+ *
3
+ * Pre-R170 clicking Ring/Grid teleported every node to its new
4
+ * position in one frame — most jarring user action on the canvas.
5
+ *
6
+ * R170 introduces layoutSwitching one-shot flag (same pattern as
7
+ * R168 smoothView but on opacity axis). The viewport <g>:
8
+ * transition: 'opacity 250ms ease-out' (always present)
9
+ * + 'transform 300ms ease-out' (when smoothView armed)
10
+ * opacity: layoutSwitching ? 0.45 : 1
11
+ * data-topo-viewport-layout-switching: 'true' | 'false'
12
+ *
13
+ * Layout state swap is synchronous (setLayout) — the dim masks
14
+ * the snap as a soft blink. Auto-clears 400ms (covers fade-down
15
+ * 250ms + buffer for React commit).
16
+ *
17
+ * Test:
18
+ * 1. Idle (ring layout): layoutSwitching='false', opacity=1
19
+ * 2. Press 'L' (toggleLayout) → flag='true', opacity=0.45
20
+ * 3. Layout state flipped to grid (group boxes visible)
21
+ * 4. Wait 450ms → flag='false', opacity=1
22
+ * 5. Press 'L' again → flag='true' (reverse direction works too)
23
+ */
24
+ import { chromium } from 'playwright';
25
+ import { readFileSync } from 'node:fs';
26
+
27
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
28
+ const browser = await chromium.launch({ headless: true });
29
+
30
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
31
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
32
+ await ctx.addInitScript(() => {
33
+ try {
34
+ localStorage.setItem('anet-theme', 'cyber');
35
+ sessionStorage.setItem('anet_v3_auth', '1');
36
+ localStorage.setItem('anet-topo-layout', 'ring');
37
+ } catch {}
38
+ });
39
+
40
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
41
+ await ctx.route('**/api/hub/status*', async (route) => {
42
+ const r = await route.fetch();
43
+ const b = await r.json();
44
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
45
+ const mk = (alias) => ({
46
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'cli-claude-code',
47
+ network_id: nid, project_dir: null,
48
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
49
+ });
50
+ // Prefix-clustered aliases so group boxes render in grid layout
51
+ await route.fulfill({ response: r, json: { ...b, sessions: [
52
+ mk('agents-a1'), mk('agents-a2'), mk('infra-b1'), mk('infra-b2'),
53
+ ] } });
54
+ });
55
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
56
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
57
+
58
+ const page = await ctx.newPage();
59
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
60
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
61
+ await page.waitForSelector('[data-topo-viewport]', { timeout: 10000 });
62
+ await page.waitForTimeout(400);
63
+
64
+ const probeViewport = () => page.evaluate(() => {
65
+ const g = document.querySelector('[data-topo-viewport]');
66
+ return {
67
+ layoutSwitching: g?.getAttribute('data-topo-viewport-layout-switching'),
68
+ opacity: g?.style?.opacity || '',
69
+ transition: g?.style?.transition || '',
70
+ hubVisible: !!document.querySelector('[data-topo-hub]'),
71
+ groupBoxes: document.querySelectorAll('[data-group-box-pinned]').length,
72
+ };
73
+ });
74
+
75
+ const idle = await probeViewport();
76
+
77
+ // Press 'L' to toggle ring → grid. Probe within 400ms window.
78
+ await page.keyboard.press('l');
79
+ await page.waitForTimeout(50);
80
+ const duringToggle1 = await probeViewport();
81
+
82
+ // Wait past 400ms window
83
+ await page.waitForTimeout(420);
84
+ const afterToggle1 = await probeViewport();
85
+
86
+ // Press 'L' again to toggle grid → ring
87
+ await page.keyboard.press('l');
88
+ await page.waitForTimeout(50);
89
+ const duringToggle2 = await probeViewport();
90
+
91
+ await page.waitForTimeout(420);
92
+ const afterToggle2 = await probeViewport();
93
+
94
+ await browser.close();
95
+
96
+ const results = {
97
+ viewport_found: idle !== null,
98
+ idle_layoutSwitching_false: idle.layoutSwitching === 'false',
99
+ idle_opacity_1: idle.opacity === '1' || idle.opacity === '',
100
+ idle_has_opacity_transition: idle.transition.includes('opacity 250ms'),
101
+ idle_in_ring: idle.hubVisible && idle.groupBoxes === 0,
102
+
103
+ toggle1_flag_true: duringToggle1.layoutSwitching === 'true',
104
+ toggle1_opacity_0p45: duringToggle1.opacity === '0.45',
105
+ // Layout state has flipped synchronously inside toggleLayout
106
+ toggle1_now_grid: !duringToggle1.hubVisible && duringToggle1.groupBoxes > 0,
107
+
108
+ after1_flag_false: afterToggle1.layoutSwitching === 'false',
109
+ after1_opacity_1: afterToggle1.opacity === '1',
110
+ after1_still_grid: !afterToggle1.hubVisible && afterToggle1.groupBoxes > 0,
111
+
112
+ toggle2_flag_true: duringToggle2.layoutSwitching === 'true',
113
+ toggle2_opacity_0p45: duringToggle2.opacity === '0.45',
114
+ toggle2_back_to_ring: duringToggle2.hubVisible && duringToggle2.groupBoxes === 0,
115
+
116
+ after2_flag_false: afterToggle2.layoutSwitching === 'false',
117
+ after2_opacity_1: afterToggle2.opacity === '1',
118
+ };
119
+ const ok = Object.values(results).every(Boolean);
120
+ console.log(`${ok ? '✅' : '❌'} layout switch crossfade:`, JSON.stringify(results),
121
+ `\n idle =`, idle,
122
+ `\n during toggle1 (ring→grid) =`, duringToggle1,
123
+ `\n after toggle1 =`, afterToggle1,
124
+ `\n during toggle2 (grid→ring) =`, duringToggle2,
125
+ `\n after toggle2 =`, afterToggle2);
126
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,104 @@
1
+ /* Round 268 verification: Layout toggle border unified with chrome
2
+ * strip's theme-aware borderColor.
3
+ *
4
+ * Pre-R268 the Layout toggle's wrapper + Grid button's internal
5
+ * divider used hardcoded `border-gray-500/25` (Tailwind pale gray,
6
+ * fixed in both themes). The bottom-right chrome strip's analogous
7
+ * wrappers (nodeSize, zoom) used pal.containerBorder (cyber
8
+ * #2a2a4a dark indigo ↔ light #e3e6eb pale gray).
9
+ *
10
+ * Visible mismatch in cyber theme: Layout toggle border read as pale
11
+ * gray-500/25 (rgba 107,114,128,0.25) while chrome strip borders read
12
+ * as darker indigo `#2a2a4a` — two different border colors on
13
+ * visually-analogous segmented controls. R268 replaces the hardcoded
14
+ * class with inline pal.containerBorder + a border-color transition.
15
+ *
16
+ * Test scope:
17
+ * 1. Layout toggle wrapper rgb(border-color) matches nodeSize
18
+ * wrapper rgb(border-color) — both use pal.containerBorder.
19
+ * 2. Layout toggle wrapper has border-color transition (200ms).
20
+ * 3. Grid button's left border color matches the wrapper's
21
+ * border color.
22
+ * 4. R260 chip-row semantic gap intact: gap toggle→working ≥ 11.
23
+ * 5. R267 title-block leading-tight regression: kicker still has
24
+ * the class.
25
+ */
26
+ import { chromium } from 'playwright';
27
+ import { readFileSync } from 'node:fs';
28
+
29
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
30
+ const browser = await chromium.launch({ headless: true });
31
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
32
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
33
+ await ctx.addInitScript(() => {
34
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
35
+ });
36
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
37
+ await ctx.route('**/api/hub/status*', async (route) => {
38
+ const r = await route.fetch();
39
+ const b = await r.json();
40
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
41
+ const mk = (alias) => ({
42
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
43
+ network_id: nid, project_dir: null,
44
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
45
+ });
46
+ await route.fulfill({ response: r, json: { ...b, sessions: [
47
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
48
+ ] } });
49
+ });
50
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
51
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
52
+
53
+ const page = await ctx.newPage();
54
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
55
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
56
+ await page.waitForSelector('[data-topo-chrome-layout-trailer]', { timeout: 10000 });
57
+ await page.waitForSelector('[data-topo-chrome-fleet-group-trailer]', { timeout: 10000 });
58
+ await page.waitForSelector('[data-topo-chrome-layout="grid"]', { timeout: 10000 });
59
+ await page.waitForSelector('[data-topo-section-kicker]', { timeout: 10000 });
60
+ await page.waitForSelector('[data-working-chip]', { timeout: 10000 });
61
+ await page.waitForTimeout(300);
62
+
63
+ const probe = await page.evaluate(() => {
64
+ const layoutWrapper = document.querySelector('[data-topo-chrome-layout-trailer]');
65
+ const nodeSize = document.querySelector('[data-topo-chrome-fleet-group-trailer]');
66
+ const gridButton = document.querySelector('[data-topo-chrome-layout="grid"]');
67
+ const kicker = document.querySelector('[data-topo-section-kicker]');
68
+ const working = document.querySelector('[data-working-chip]');
69
+ const layoutStyle = layoutWrapper ? window.getComputedStyle(layoutWrapper) : null;
70
+ const nodeSizeStyle = nodeSize ? window.getComputedStyle(nodeSize) : null;
71
+ const gridStyle = gridButton ? window.getComputedStyle(gridButton) : null;
72
+ return {
73
+ layoutBorderColor: layoutStyle ? layoutStyle.borderTopColor : null,
74
+ layoutTransition: layoutWrapper ? layoutWrapper.style.transition : null,
75
+ nodeSizeBorderColor: nodeSizeStyle ? nodeSizeStyle.borderTopColor : null,
76
+ gridBorderLeftColor: gridStyle ? gridStyle.borderLeftColor : null,
77
+ kickerHasLeadingTight: kicker?.classList.contains('leading-tight') ?? false,
78
+ toggleRight: layoutWrapper ? layoutWrapper.getBoundingClientRect().right : null,
79
+ workingLeft: working ? working.getBoundingClientRect().left : null,
80
+ };
81
+ });
82
+ await browser.close();
83
+
84
+ const has = (s, prop) => new RegExp(`${prop}\\s+(?:200ms|0\\.2s)`).test(s || '');
85
+ const gapToggleToWorking = (probe.toggleRight != null && probe.workingLeft != null)
86
+ ? (probe.workingLeft - probe.toggleRight) : null;
87
+
88
+ const results = {
89
+ layout_border_matches_nodesize: probe.layoutBorderColor != null
90
+ && probe.layoutBorderColor === probe.nodeSizeBorderColor,
91
+ layout_has_border_color_transition: has(probe.layoutTransition, 'border-color'),
92
+ grid_button_border_matches_wrapper: probe.gridBorderLeftColor != null
93
+ && probe.gridBorderLeftColor === probe.layoutBorderColor,
94
+ r260_chip_row_gap_intact: gapToggleToWorking != null && gapToggleToWorking >= 11,
95
+ r267_kicker_has_leading_tight: probe.kickerHasLeadingTight,
96
+ };
97
+ const ok = Object.values(results).every(Boolean);
98
+ console.log(`${ok ? '✅' : '❌'} layout toggle border unify:`, JSON.stringify(results),
99
+ '\n layout borderTop:', probe.layoutBorderColor,
100
+ '\n nodeSize borderTop:', probe.nodeSizeBorderColor,
101
+ '\n grid btn borderLeft:', probe.gridBorderLeftColor,
102
+ '\n layout transition:', probe.layoutTransition,
103
+ '\n toggle→working gap:', gapToggleToWorking);
104
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,131 @@
1
+ /* Round 163 verification: Ring/Grid layout toggle buttons pick
2
+ * up the R154 chrome-button polish convention.
3
+ *
4
+ * Pre-R163 the buttons had aria-pressed + transition-colors but:
5
+ * - no focus-visible ring (browser default invisible on canvas)
6
+ * - inactive hover only nudged text-gray-500 → gray-400, no bg
7
+ *
8
+ * R163 adds:
9
+ * data-topo-chrome-layout="ring|grid"
10
+ * data-topo-chrome-layout-active="true|false"
11
+ * focus-visible:ring-2 ring-cyan-400/60 ring-inset
12
+ * inactive: hover:text-gray-300 + hover:bg-cyan-500/5
13
+ * active: hover:bg-cyan-500/20 (slightly hotter)
14
+ *
15
+ * Test:
16
+ * 1. Both buttons present with data-topo-chrome-layout attr
17
+ * 2. Active button has bg-cyan-500/15 class fragment (existing)
18
+ * 3. Focus the inactive button → focus-visible ring class fragment
19
+ * 4. Hover the inactive button → bg color shifts from transparent
20
+ * to a cyan-ish tint (border bg-cyan-500/5)
21
+ */
22
+ import { chromium } from 'playwright';
23
+ import { readFileSync } from 'node:fs';
24
+
25
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
26
+ const browser = await chromium.launch({ headless: true });
27
+
28
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
29
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
30
+ await ctx.addInitScript(() => {
31
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); localStorage.setItem('anet-topo-layout', 'ring'); } catch {}
32
+ });
33
+
34
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
35
+ await ctx.route('**/api/hub/status*', async (route) => {
36
+ const r = await route.fetch();
37
+ const b = await r.json();
38
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
39
+ const mk = (alias) => ({
40
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'cli-claude-code',
41
+ network_id: nid, project_dir: null,
42
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
43
+ });
44
+ await route.fulfill({ response: r, json: { ...b, sessions: [mk('alpha'), mk('beta')] } });
45
+ });
46
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
47
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
48
+
49
+ const page = await ctx.newPage();
50
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
51
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 2, { timeout: 30000 });
52
+ await page.waitForSelector('[data-topo-chrome-layout]', { timeout: 10000 });
53
+ await page.waitForTimeout(400);
54
+
55
+ // Probe basic attrs & class membership
56
+ const probe = await page.evaluate(() => {
57
+ const ring = document.querySelector('[data-topo-chrome-layout="ring"]');
58
+ const grid = document.querySelector('[data-topo-chrome-layout="grid"]');
59
+ if (!ring || !grid) return null;
60
+ return {
61
+ ring_active: ring.getAttribute('data-topo-chrome-layout-active'),
62
+ grid_active: grid.getAttribute('data-topo-chrome-layout-active'),
63
+ ring_pressed: ring.getAttribute('aria-pressed'),
64
+ grid_pressed: grid.getAttribute('aria-pressed'),
65
+ ring_class: ring.getAttribute('class') || '',
66
+ grid_class: grid.getAttribute('class') || '',
67
+ };
68
+ });
69
+
70
+ // Focus the inactive (grid) button — focus-visible ring should kick in.
71
+ // Tab-focus produces the :focus-visible state; .focus() doesn't always.
72
+ // Use page.keyboard.press('Tab') after first focusing somewhere known.
73
+ await page.locator('[data-topo-chrome-layout="grid"]').focus();
74
+ // trigger keyboard navigation to enable focus-visible state heuristic
75
+ await page.evaluate(() => {
76
+ const el = document.querySelector('[data-topo-chrome-layout="grid"]');
77
+ if (el) el.matches(':focus-visible');
78
+ });
79
+ const focusVisibleProbe = await page.evaluate(() => {
80
+ const el = document.querySelector('[data-topo-chrome-layout="grid"]');
81
+ if (!el) return null;
82
+ return {
83
+ isActiveElement: document.activeElement === el,
84
+ // Inspect outline & box-shadow because the Tailwind focus-visible:ring
85
+ // class compiles to a box-shadow. After .focus() Chromium reports
86
+ // :focus-visible heuristically — we verify the class fragment exists
87
+ // in the stylesheet bound to this element rather than relying on the
88
+ // dynamic pseudo-class match.
89
+ classFragments: {
90
+ hasFocusVisibleRingClass: (el.getAttribute('class') || '').includes('focus-visible:ring-cyan-400/60'),
91
+ hasRingInset: (el.getAttribute('class') || '').includes('focus-visible:ring-inset'),
92
+ hasFocusOutlineNone: (el.getAttribute('class') || '').includes('focus:outline-none'),
93
+ },
94
+ };
95
+ });
96
+
97
+ // Hover the inactive (grid) button — bg should shift away from transparent.
98
+ const gridBgBefore = await page.evaluate(() => {
99
+ const el = document.querySelector('[data-topo-chrome-layout="grid"]');
100
+ return el ? getComputedStyle(el).backgroundColor : null;
101
+ });
102
+ await page.locator('[data-topo-chrome-layout="grid"]').hover();
103
+ await page.waitForTimeout(300);
104
+ const gridBgHover = await page.evaluate(() => {
105
+ const el = document.querySelector('[data-topo-chrome-layout="grid"]');
106
+ return el ? getComputedStyle(el).backgroundColor : null;
107
+ });
108
+
109
+ await browser.close();
110
+
111
+ const results = {
112
+ both_buttons_found: probe !== null,
113
+ ring_is_active: probe?.ring_active === 'true',
114
+ grid_is_inactive: probe?.grid_active === 'false',
115
+ ring_aria_pressed: probe?.ring_pressed === 'true',
116
+ grid_aria_unpressed: probe?.grid_pressed === 'false',
117
+ active_has_cyan_bg_class: probe?.ring_class.includes('bg-cyan-500/15'),
118
+ inactive_hover_bg_class: probe?.grid_class.includes('hover:bg-cyan-500/5'),
119
+ active_hover_bg_class: probe?.ring_class.includes('hover:bg-cyan-500/20'),
120
+ focus_outline_none: focusVisibleProbe?.classFragments?.hasFocusOutlineNone === true,
121
+ focus_visible_ring_class: focusVisibleProbe?.classFragments?.hasFocusVisibleRingClass === true,
122
+ focus_visible_ring_inset: focusVisibleProbe?.classFragments?.hasRingInset === true,
123
+ inactive_bg_before: gridBgBefore !== null,
124
+ hover_changes_bg: gridBgBefore !== gridBgHover,
125
+ };
126
+ const ok = Object.values(results).every(Boolean);
127
+ console.log(`${ok ? '✅' : '❌'} layout toggle polish:`, JSON.stringify(results),
128
+ `\n probe=`, probe,
129
+ `\n focus=`, focusVisibleProbe,
130
+ `\n bg before/hover=`, gridBgBefore, '→', gridBgHover);
131
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,107 @@
1
+ /* Round 277 verification: legend panel compress 104 → 88, matching the
2
+ * recent-signal panel's height post-R256. Row stride 24 → 18.
3
+ *
4
+ * Pre-R277:
5
+ * Panel: 224×104, rx=8
6
+ * Row 1 (working): y0=32, y1=36
7
+ * Row 2 (idle): y0=56, y1=60
8
+ * Row 3 (offline): y0=80, y1=84
9
+ * Flow-arrow: M140,80 Q164,56 196,80 (endpoints y=80)
10
+ *
11
+ * Post-R277:
12
+ * Panel: 224×88, rx=8
13
+ * Row 1 (working): y0=32, y1=36 (anchored — R271 hitbox y=21 invariant)
14
+ * Row 2 (idle): y0=50, y1=54 (-6)
15
+ * Row 3 (offline): y0=68, y1=72 (-12)
16
+ * Flow-arrow: M140,68 Q164,44 196,68 (tracks new offline cy)
17
+ *
18
+ * Net: legend panel takes ~15% less vertical chrome; panel pair
19
+ * (recent-signal 88 + legend 88) now share the same height. Third
20
+ * 减法 cut after R275 (FreshnessChip conditional) + R276 (orbit
21
+ * particles retired).
22
+ *
23
+ * Test scope:
24
+ * 1. Legend panel rect height === 88.
25
+ * 2. Recent-signal panel rect height === 88 (regression — R256 invariant).
26
+ * 3. Working row hitbox y === 21 (R271 invariant: row.y0=32 unchanged).
27
+ * 4. Flow-arrow path starts with "M140,68" (tracks new offline cy=68).
28
+ * 5. R275 freshness chip absent at fresh (regression).
29
+ * 6. R276 orbit particles absent (regression).
30
+ */
31
+ import { chromium } from 'playwright';
32
+ import { readFileSync } from 'node:fs';
33
+
34
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
35
+ const browser = await chromium.launch({ headless: true });
36
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
37
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
38
+ await ctx.addInitScript(() => {
39
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
40
+ });
41
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
42
+ await ctx.route('**/api/hub/status*', async (route) => {
43
+ const r = await route.fetch();
44
+ const b = await r.json();
45
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
46
+ const mk = (alias) => ({
47
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
48
+ network_id: nid, project_dir: null,
49
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
50
+ });
51
+ await route.fulfill({ response: r, json: { ...b, sessions: [
52
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
53
+ ] } });
54
+ });
55
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
56
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
57
+
58
+ const page = await ctx.newPage();
59
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
60
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
61
+ await page.waitForSelector('[data-topo-panel="legend"]', { timeout: 10000 });
62
+ await page.waitForSelector('[data-topo-panel="recent"]', { timeout: 10000 });
63
+ await page.waitForSelector('[data-legend-status="working"] rect', { timeout: 10000 });
64
+ await page.waitForSelector('[data-legend-flow-arrow]', { timeout: 10000 });
65
+ await page.waitForTimeout(300);
66
+
67
+ // Hover working row to materialize the tinted hitbox rect for the
68
+ // y=21 invariant check (R271). Without hover the rect carries the
69
+ // same y attr but tint fills are transparent.
70
+ await page.locator('[data-legend-status="working"]').hover();
71
+ await page.waitForTimeout(150);
72
+
73
+ const probe = await page.evaluate(() => {
74
+ const legendG = document.querySelector('[data-topo-panel="legend"]');
75
+ const recentG = document.querySelector('[data-topo-panel="recent"]');
76
+ const workingRow = document.querySelector('[data-legend-status="working"]');
77
+ const workingHitbox = workingRow ? workingRow.querySelector('rect') : null;
78
+ const flowArrow = document.querySelector('[data-legend-flow-arrow]');
79
+ const freshnessChip = document.querySelector('[data-freshness-chip]');
80
+ const orbits = document.querySelectorAll('[data-topo-orbit-bucket]');
81
+ return {
82
+ legendHeight: legendG?.querySelector('rect')?.getAttribute('height') ?? null,
83
+ recentHeight: recentG?.querySelector('rect')?.getAttribute('height') ?? null,
84
+ workingHitboxY: workingHitbox ? +workingHitbox.getAttribute('y') : null,
85
+ flowArrowD: flowArrow ? flowArrow.getAttribute('d') : null,
86
+ freshnessPresent: freshnessChip !== null,
87
+ orbitCount: orbits.length,
88
+ };
89
+ });
90
+ await browser.close();
91
+
92
+ const results = {
93
+ legend_height_88: probe.legendHeight === '88',
94
+ recent_height_88_regression: probe.recentHeight === '88',
95
+ r271_working_hitbox_y_21: probe.workingHitboxY === 21,
96
+ flow_arrow_starts_at_y68: probe.flowArrowD === 'M140,68 Q164,44 196,68',
97
+ r275_freshness_absent: probe.freshnessPresent === false,
98
+ r276_orbit_absent: probe.orbitCount === 0,
99
+ };
100
+ const ok = Object.values(results).every(Boolean);
101
+ console.log(`${ok ? '✅' : '❌'} legend compress:`, JSON.stringify(results),
102
+ '\n legend height:', probe.legendHeight,
103
+ '\n recent height:', probe.recentHeight,
104
+ '\n working hitbox y:', probe.workingHitboxY,
105
+ '\n flow arrow d:', probe.flowArrowD,
106
+ '\n orbit count:', probe.orbitCount);
107
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,126 @@
1
+ /* Round 204 verification: legend count text recedes when its status
2
+ * tier is empty (count = 0). Pre-R204 "0" at opacity 0.65 read
3
+ * visually identical to "12" at opacity 0.65; R204 drops empty rows
4
+ * to 0.30 (dark) / 0.28 (light) so the eye gets a "no nodes in this
5
+ * tier" signal without parsing the digit.
6
+ *
7
+ * Test (cyber dark, all 4 sessions = working → idle=0, offline=0):
8
+ * 1. Three legend count texts present with data-legend-count for
9
+ * working / idle / offline.
10
+ * 2. Working row: count='4', data-legend-count-empty='false',
11
+ * opacity = 0.65 (idle, not hovered).
12
+ * 3. Idle row: count='0', data-legend-count-empty='true',
13
+ * opacity ≈ 0.30 (dim recede).
14
+ * 4. Offline row: count='0', data-legend-count-empty='true',
15
+ * opacity ≈ 0.30.
16
+ * 5. Hover working row → its count bumps to opacity ~0.95
17
+ * (existing R95 hover behaviour unbroken).
18
+ * 6. Hover idle row → empty-tier opacity DOES NOT bump (empty
19
+ * gate wins over hover). Recedes regardless.
20
+ * 7. Transition: opacity 150ms ease-out still on all three.
21
+ */
22
+ import { chromium } from 'playwright';
23
+ import { readFileSync } from 'node:fs';
24
+
25
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
26
+ const browser = await chromium.launch({ headless: true });
27
+
28
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
29
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
30
+ await ctx.addInitScript(() => {
31
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
32
+ });
33
+
34
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
35
+ await ctx.route('**/api/hub/status*', async (route) => {
36
+ const r = await route.fetch();
37
+ const b = await r.json();
38
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
39
+ // All 4 working — idle / offline tiers empty
40
+ const mk = (alias) => ({
41
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'cli-claude-code',
42
+ network_id: nid, project_dir: null,
43
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
44
+ });
45
+ await route.fulfill({ response: r, json: { ...b, sessions: [
46
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
47
+ ] } });
48
+ });
49
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
50
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
51
+
52
+ const page = await ctx.newPage();
53
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
54
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
55
+ await page.waitForSelector('[data-legend-count="working"]', { timeout: 10000 });
56
+ await page.waitForTimeout(300);
57
+
58
+ const probe = async () => page.evaluate(() => {
59
+ const pick = (key) => {
60
+ const el = document.querySelector(`[data-legend-count="${key}"]`);
61
+ if (!el) return null;
62
+ return {
63
+ key,
64
+ text: el.textContent,
65
+ empty: el.getAttribute('data-legend-count-empty'),
66
+ opacity: parseFloat(el.getAttribute('opacity') || ''),
67
+ transition: el.style.transition,
68
+ };
69
+ };
70
+ return { working: pick('working'), idle: pick('idle'), offline: pick('offline') };
71
+ });
72
+
73
+ const idle = await probe();
74
+
75
+ // Hover the working row — verify R95 hover bump still fires (count > 0)
76
+ await page.hover('[data-legend-status="working"]');
77
+ await page.waitForTimeout(200);
78
+ const hovWorking = await probe();
79
+
80
+ // Move off, then hover the empty (idle=0) row — verify empty gate wins
81
+ await page.mouse.move(0, 0);
82
+ await page.waitForTimeout(200);
83
+ await page.hover('[data-legend-status="idle"]');
84
+ await page.waitForTimeout(200);
85
+ const hovIdle = await probe();
86
+
87
+ await page.mouse.move(0, 0);
88
+ await page.waitForTimeout(200);
89
+
90
+ await browser.close();
91
+
92
+ const opacityNear = (got, want, tol = 0.04) => Math.abs(got - want) < tol;
93
+
94
+ const results = {
95
+ working_present: !!idle?.working,
96
+ idle_present: !!idle?.idle,
97
+ offline_present: !!idle?.offline,
98
+
99
+ working_text_4: idle?.working?.text === '4',
100
+ working_not_empty: idle?.working?.empty === 'false',
101
+ working_opacity_0p65: opacityNear(idle?.working?.opacity ?? 0, 0.65),
102
+
103
+ idle_text_0: idle?.idle?.text === '0',
104
+ idle_empty_true: idle?.idle?.empty === 'true',
105
+ idle_opacity_dim: opacityNear(idle?.idle?.opacity ?? 0, 0.30),
106
+
107
+ offline_text_0: idle?.offline?.text === '0',
108
+ offline_empty_true: idle?.offline?.empty === 'true',
109
+ offline_opacity_dim: opacityNear(idle?.offline?.opacity ?? 0, 0.30),
110
+
111
+ // R95 hover still bumps when row populated
112
+ working_hover_opacity_high: opacityNear(hovWorking?.working?.opacity ?? 0, 0.95),
113
+
114
+ // Empty-gate wins: hovering an empty row does NOT bump its count opacity
115
+ idle_hover_stays_dim: opacityNear(hovIdle?.idle?.opacity ?? 0, 0.30),
116
+
117
+ // Transition still in place across all 3
118
+ all_have_transition: ['working', 'idle', 'offline'].every(k =>
119
+ /opacity\s+(?:150ms|0\.15s)/.test(idle?.[k]?.transition || '')),
120
+ };
121
+ const ok = Object.values(results).every(Boolean);
122
+ console.log(`${ok ? '✅' : '❌'} legend count empty:`, JSON.stringify(results),
123
+ `\n idle:`, idle,
124
+ `\n hovWorking:`, hovWorking,
125
+ `\n hovIdle:`, hovIdle);
126
+ process.exit(ok ? 0 : 1);