@sleep2agi/agent-network-dashboard 0.5.1-preview.99 → 0.5.1

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 (201) 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 +32 -32
  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.js.nft.json +1 -1
  84. package/.next/server/app/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  86. package/.next/server/app/server-logs.html +2 -2
  87. package/.next/server/app/server-logs.rsc +14 -14
  88. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  89. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  90. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  91. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  92. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  93. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  94. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  95. package/.next/server/app/settings/networks.html +2 -2
  96. package/.next/server/app/settings/networks.rsc +14 -14
  97. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  98. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  99. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  100. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  101. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  102. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  103. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  104. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  105. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  106. package/.next/server/app/settings/tokens.html +2 -2
  107. package/.next/server/app/settings/tokens.rsc +14 -14
  108. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  109. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  110. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  111. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  112. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  113. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  114. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  115. package/.next/server/app/settings.html +2 -2
  116. package/.next/server/app/settings.rsc +14 -14
  117. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  118. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  119. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  120. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  121. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  122. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  123. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  125. package/.next/server/app/tasks.html +2 -2
  126. package/.next/server/app/tasks.rsc +14 -14
  127. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  128. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  129. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  130. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  131. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  132. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  133. package/.next/server/chunks/ssr/{[root-of-the-server]__03b.f76._.js → [root-of-the-server]__0sv~g.o._.js} +2 -2
  134. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js.map +1 -0
  135. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  136. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  137. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  138. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  139. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  140. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  141. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  142. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  143. package/.next/server/middleware-build-manifest.js +3 -3
  144. package/.next/server/pages/404.html +2 -2
  145. package/.next/server/pages/500.html +1 -1
  146. package/.next/server/server-reference-manifest.js +1 -1
  147. package/.next/server/server-reference-manifest.json +1 -1
  148. package/.next/static/chunks/{0l_~q07bhpkcx.js → 03a4--7ncekmk.js} +1 -1
  149. package/.next/static/chunks/05admfiu6qfp2.js +1 -0
  150. package/.next/static/chunks/0a4hmfvj-81x5.css +2 -0
  151. package/.next/static/chunks/0d9mlqf.rjey5.js +1 -0
  152. package/.next/static/chunks/11puuje6at2jt.js +4 -0
  153. package/.next/static/chunks/{03~~oirxz7~vc.js → 15ub0_3b099x1.js} +1 -1
  154. package/.next/trace +2 -2
  155. package/.next/trace-build +1 -1
  156. package/app/components/ServersDrawer.tsx +17 -0
  157. package/app/components/TopoGraph.tsx +722 -76
  158. package/package.json +1 -1
  159. package/screenshots/v0.10.2-disk-verify/disk-render-full.png +0 -0
  160. package/screenshots/v0.10.2-disk-verify/disk-render.png +0 -0
  161. package/screenshots/v0.11.0-147/after.png +0 -0
  162. package/scripts/p0-147-screenshot.mjs +53 -0
  163. package/scripts/p0-servers-drawer-screenshot.mjs +2 -0
  164. package/scripts/topo-any-hover-attr-test.mjs +83 -0
  165. package/scripts/topo-any-pinned-attr-test.mjs +86 -0
  166. package/scripts/topo-chrome-fullscreen-icon-sw-test.mjs +92 -0
  167. package/scripts/topo-chrome-reset-icon-sw-test.mjs +80 -0
  168. package/scripts/topo-chrome-zoom-icon-sw-test.mjs +90 -0
  169. package/scripts/topo-dashboard-version-attr-test.mjs +69 -0
  170. package/scripts/topo-dense-alias-opacity-test.mjs +68 -0
  171. package/scripts/topo-edge-particle-hover-r-test.mjs +113 -0
  172. package/scripts/topo-endpoint-ring-r-hover-test.mjs +89 -0
  173. package/scripts/topo-group-box-geom-transition-test.mjs +110 -0
  174. package/scripts/topo-group-box-rx-pin-test.mjs +103 -0
  175. package/scripts/topo-group-label-count-fw-test.mjs +100 -0
  176. package/scripts/topo-group-label-fw-pin-test.mjs +99 -0
  177. package/scripts/topo-group-label-tint-geom-test.mjs +94 -0
  178. package/scripts/topo-group-label-tint-transition-test.mjs +97 -0
  179. package/scripts/topo-group-pip-fontsize-test.mjs +106 -0
  180. package/scripts/topo-group-tint-rx-pin-test.mjs +107 -0
  181. package/scripts/topo-hub-core-fill-hover-test.mjs +85 -0
  182. package/scripts/topo-hub-halo-r-hover-test.mjs +82 -0
  183. package/scripts/topo-legend-count-active-opacity-test.mjs +102 -0
  184. package/scripts/topo-legend-count-pin-fw-test.mjs +90 -0
  185. package/scripts/topo-minimap-viewport-opacity-test.mjs +96 -0
  186. package/scripts/topo-node-halo-hover-opacity-test.mjs +104 -0
  187. package/scripts/topo-node-halo-light-offline-test.mjs +80 -0
  188. package/scripts/topo-node-sub-text-fw-test.mjs +75 -0
  189. package/scripts/topo-overlap-stale-build-guard-test.mjs +66 -0
  190. package/scripts/topo-overlap-test.mjs +42 -1
  191. package/scripts/topo-recent-row-count-pin-fw-test.mjs +106 -0
  192. package/scripts/topo-recent-row-pip-hover-r-test.mjs +104 -0
  193. package/scripts/topo-runtime-icon-hover-test.mjs +96 -0
  194. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js.map +0 -1
  195. package/.next/static/chunks/0.sf46gnv4wwm.js +0 -1
  196. package/.next/static/chunks/017hq2-5l~_98.css +0 -2
  197. package/.next/static/chunks/0_igeywsok2_-.js +0 -1
  198. package/.next/static/chunks/0igz0bww16uvc.js +0 -4
  199. /package/.next/static/{4rHxTzLwe2XC9M1rN-MpJ → gowHgWkn_kpMCSBn8RFaa}/_buildManifest.js +0 -0
  200. /package/.next/static/{4rHxTzLwe2XC9M1rN-MpJ → gowHgWkn_kpMCSBn8RFaa}/_clientMiddlewareManifest.js +0 -0
  201. /package/.next/static/{4rHxTzLwe2XC9M1rN-MpJ → gowHgWkn_kpMCSBn8RFaa}/_ssgManifest.js +0 -0
@@ -8,6 +8,7 @@ import { aliasAvatarColors, aliasInitial } from './AliasAvatar';
8
8
  import { ChatPopover } from './ChatPopover';
9
9
  import { vendorForModel, runtimeIdentity, identityLine } from '../lib/vendorIdentity';
10
10
  import { parseHubTime, relativeAgo } from '../lib/time';
11
+ import { DASHBOARD_VERSION } from '../lib/version';
11
12
 
12
13
  /** v0.10.0 Hero 1+2 / §3.F server-health hook — fetches the normalized
13
14
  * /api/hub/servers payload (preview.370 unblocked real-data via the
@@ -3457,6 +3458,81 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3457
3458
  return `Agent network topology — ${parts.join(' · ')}. Tab to navigate nodes, double-click canvas to reset view.`;
3458
3459
  })()}
3459
3460
  data-topo-canvas-aria
3461
+ /* Round 466 / Loop — aggregate hover signal on the root SVG.
3462
+ Exposes a single boolean `data-topo-any-hover` that
3463
+ reflects whether ANY hover state in the topology is
3464
+ active. Composed from the existing per-surface hover
3465
+ vars; doesn't introduce new state. Useful for:
3466
+ - Playwright tests asserting "topology entered a hover
3467
+ mode" without enumerating per-surface attrs
3468
+ - external CSS hooks targeting `[data-topo-any-hover=
3469
+ "true"]` to dim adjacent UI (e.g. chrome strip)
3470
+ while the user is inspecting the canvas
3471
+ - debug overlays that visualise hover dwell-time
3472
+ The 6 hover sources contributing:
3473
+ hoveredAlias (node circle / card / alias text)
3474
+ hoveredHub (hub center, halo, ring)
3475
+ hoveredEdgeKey (flow link path / particle / endpoint)
3476
+ hoveredGroupLabel (cluster name / count / pips)
3477
+ hoveredStatus (legend row)
3478
+ hoveredVendor (vendor chip in chip row)
3479
+ Read-only computed attr — zero re-render cost beyond the
3480
+ React update that already fires when any of those state
3481
+ vars flips. Geometry / paint untouched. */
3482
+ data-topo-any-hover={
3483
+ (hoveredAlias || hoveredHub || hoveredEdgeKey || hoveredGroupLabel ||
3484
+ hoveredStatus || hoveredVendor) ? 'true' : 'false'
3485
+ }
3486
+ /* Round 467 / Loop — pin-aggregate sibling to R466 hover-
3487
+ aggregate. Exposes `data-topo-any-pinned` reflecting
3488
+ whether ANY sticky inspection mode is active. Composed
3489
+ from the 4 pinned state vars:
3490
+ pinnedStatus (legend row click → status filter)
3491
+ pinnedGroup (group label click → cluster lock)
3492
+ pinnedVendor (vendor chip click → vendor filter)
3493
+ pinnedEdgeKey (edge click → edge focus)
3494
+ Together with R466 the root svg now carries a 2-bit
3495
+ inspection-mode surface:
3496
+ data-topo-any-hover — transient (mouse hover)
3497
+ data-topo-any-pinned — sticky (click-to-lock)
3498
+ Useful for:
3499
+ - Playwright tests: one-line query for either mode
3500
+ - external CSS hooks: render a persistent "filter
3501
+ active" badge when pinned, distinct from the
3502
+ transient hover dim
3503
+ - Esc-handler tests: assert all 4 pins clear after
3504
+ the universal-cancel Escape press (R62/R63/R88/
3505
+ R116 — single Esc collapses every pin)
3506
+ Read-only computed disjunction; no new state, zero
3507
+ re-render cost beyond the React pin-flip updates. */
3508
+ data-topo-any-pinned={
3509
+ (pinnedStatus || pinnedGroup || pinnedVendor || pinnedEdgeKey) ? 'true' : 'false'
3510
+ }
3511
+ /* Round 462 / Loop — surface DASHBOARD_VERSION on the root SVG
3512
+ element as `data-dashboard-version`. Directly closes the
3513
+ feedback_dash_zombie_port_3000.md memory rule: "verify ships
3514
+ via SVG DOM, not tmux 'Ready' — zombie next-servers + stale
3515
+ global installs silently serve old code". Pre-R462 the only
3516
+ ways to know which preview the dash was serving were:
3517
+ 1. parse the npm registry for the latest tag (network)
3518
+ 2. fetch /api/dashboard/version (API surface, no DOM)
3519
+ 3. inspect the /login footer or /settings page (off-route)
3520
+ Test scripts that probe TopoGraph DOM (overlap, group-label
3521
+ tint, pip strip, etc.) couldn't tell whether the dash was
3522
+ actually serving the build they expected to verify. R462
3523
+ threads DASHBOARD_VERSION through to the root <svg> so:
3524
+ - Playwright probes can read svg[data-dashboard-version]
3525
+ directly + fail-fast on stale-build mismatch
3526
+ - the memory rule's manual zombie check ("inspect SVG
3527
+ dom") becomes a one-attr probe
3528
+ - operators DOM-inspect to confirm the live version
3529
+ matches the npm tag without leaving the topology page
3530
+ Geometry/visual impact: ZERO (data-* attrs don't paint).
3531
+ The version string is build-time injected via the existing
3532
+ DASHBOARD_VERSION constant (R51 footer + R51 settings page
3533
+ already consume it from app/lib/version.ts → reads
3534
+ package.json pkg.version). No business logic added. */
3535
+ data-dashboard-version={DASHBOARD_VERSION}
3460
3536
  onPointerDown={onPointerDown}
3461
3537
  onPointerMove={onPointerMove}
3462
3538
  onPointerUp={onPointerUp}
@@ -4241,7 +4317,29 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4241
4317
  y={box.y}
4242
4318
  width={box.w}
4243
4319
  height={box.h}
4244
- rx="14"
4320
+ /* Round 464 / Loop: group-box rx 14 → 16 on isPinned.
4321
+ Geometric softening at the corner radius — locked
4322
+ groups read with subtly rounder shoulders than
4323
+ hovered/idle. +2px reads as a calm \"settled in\"
4324
+ posture (subtler than a fill or stroke bump but
4325
+ unmistakable across the whole cluster boundary).
4326
+ Pin signature on the group-box rect now spans 7
4327
+ axes:
4328
+ R63 text fill brighten
4329
+ R142 drop-shadow filter
4330
+ R432 text letter-spacing 0→0.5
4331
+ R444 count tspan fw 500→600
4332
+ R457 parent text fw 700→800
4333
+ codex p.125 text opacity 0.55→1
4334
+ R464 corner rx 14→16 (this round)
4335
+ transition list (R461) already covers x/y/width/
4336
+ height 200ms ease-out; appended `rx 200ms ease-
4337
+ out` so the rounding eases alongside the geometry
4338
+ axes. SVG2 CSS animation on rx: Chrome 95+ /
4339
+ Safari 16+ / FF 70+ (same matrix as x/y/w/h).
4340
+ data-group-box-rx exposes the resolved value. */
4341
+ rx={isPinned ? '16' : '14'}
4342
+ data-group-box-rx={isPinned ? '16' : '14'}
4245
4343
  fill={isLight ? '#0f172a' : '#a5b4fc'}
4246
4344
  // R68: 3-tier opacity + stroke ladder.
4247
4345
  // pinned → fill 0.08 / 0.13, stroke 3 px (locked)
@@ -4281,6 +4379,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4281
4379
  data-group-box-pinned={isPinned ? 'true' : 'false'}
4282
4380
  data-group-box-linecap="round"
4283
4381
  data-group-box-linejoin="round"
4382
+ data-group-box-geom-transition="x,y,width,height"
4284
4383
  // R85: ambient "marching ants" drift on the perimeter
4285
4384
  // when this group has at least one working member, and
4286
4385
  // neither pin nor hover is active (those treatments
@@ -4318,8 +4417,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4318
4417
  while stroke / fill-opacity / filter all eased.
4319
4418
  Closes the last theme-toggle snap on the group
4320
4419
  box surface — same idiom R246 + R247 used at
4321
- per-node label-card and side-panel scopes. */
4322
- transition: 'stroke 200ms ease-out, stroke-width 200ms ease-out, fill-opacity 200ms ease-out, filter 200ms ease-out, fill 200ms ease-out',
4420
+ per-node label-card and side-panel scopes.
4421
+ Round 461 / Loop: extend the transition list to
4422
+ all 4 geometry axes (x, y, width, height) so
4423
+ when a cluster grows / shrinks (member joins,
4424
+ leaves, prefix rebalance, dense toggle, status
4425
+ flip) the BIG outer container slides into the
4426
+ new bounds at the same 200ms cadence the R460
4427
+ inner hitbox tint rect now uses. Pre-R461 the
4428
+ outer 200×140 px box snap-jumped on cluster
4429
+ resize while the inner 160×18 hitbox slid —
4430
+ jarring two-rate motion at the same surface.
4431
+ R461 unifies both rects to slide as one, with
4432
+ the parent box driving the visual envelope and
4433
+ the inner hitbox tracking the bottom-edge tint.
4434
+ Hero D #147 motion-coherence at the FULL cluster
4435
+ container tier (not just the label tint).
4436
+ data-group-box-geom-transition attr exposed. */
4437
+ transition: 'stroke 200ms ease-out, stroke-width 200ms ease-out, fill-opacity 200ms ease-out, filter 200ms ease-out, fill 200ms ease-out, x 200ms ease-out, y 200ms ease-out, width 200ms ease-out, height 200ms ease-out, rx 200ms ease-out',
4323
4438
  pointerEvents: 'none',
4324
4439
  // CSS var consumed by `.anet-topo-groupbox-live`
4325
4440
  // (line 877 of globals.css). React's CSSProperties
@@ -4397,27 +4512,83 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4397
4512
  ].filter(Boolean).join('\n')}</title>
4398
4513
  );
4399
4514
  })()}
4515
+ {/* v0.11.0 #147 Hero D — Vincent 5401: "太大太丑,
4516
+ 都放到框的右下角的小字". First-cut bottom-right
4517
+ placement collided with bottom-row nodes (cluster
4518
+ geometry has no bottom padding; only GROUP_TOP=12
4519
+ top band). Pivoted to Option C from #147 spec:
4520
+ keep top-left anchor BUT shrink fontSize (13 → 9)
4521
+ and dim default opacity (1 → 0.55, hover/pin
4522
+ restore to 1). Satisfies "太大太丑" via the size +
4523
+ opacity axes while keeping the existing geometry
4524
+ contract that topo-overlap-test gates. Hitbox
4525
+ rect width tightens to min(box.w-12, 160) to
4526
+ track the narrower label render. */}
4527
+ {/* Round 465 / Loop — hitbox tint rect rx 4 → 5 on
4528
+ pinnedGroup match. Mirrors R464 (parent group-box
4529
+ rx 14 → 16 on isPinned) at the hitbox tier. The
4530
+ R460 hitbox carried fixed rx=4 since codex p.125
4531
+ pivoted it to the bottom-of-band position; the
4532
+ pin-state geometric softening was only on the BIG
4533
+ outer container, not the small hitbox underneath.
4534
+ R465 adds +1 px corner rounding on pin so the
4535
+ tint rect echoes the parent's locked posture at
4536
+ its own scale (8% relative bump matches R464's
4537
+ 14→16 ≈ 14% scaled to the smaller rect).
4538
+ Transition list (R460 fill/opacity/x/width 200ms
4539
+ ease-out) extends to include `rx 200ms ease-out`
4540
+ so the rounding eases under the same cadence.
4541
+ SVG2 CSS animation on rx: Chrome 95+ / Safari
4542
+ 16+ / FF 70+ (same matrix as x/y/w/h).
4543
+ data-group-label-tint-rx exposes the resolved
4544
+ value for tests. */}
4400
4545
  <rect
4401
4546
  x={box.x + 6}
4402
4547
  y={box.y + 2}
4403
- width={Math.min(box.w - 12, 240)}
4404
- height={20}
4405
- rx="4"
4406
- /* R107 / Loop: list-item tint extends to the SVG
4407
- group labels — same idiom R104 added to recent-
4408
- signal rows and R105 to the legend rows. The
4409
- tint colour is pal.legendAccent (cyan) since
4410
- groups don't carry an inherent swatch the way
4411
- legend rows do; this matches R68's group-box
4412
- isPinned/isHovered accent stroke for consistency.
4413
- hover < pinned opacity so locked vs preview is
4414
- discriminable at a glance. */
4548
+ width={Math.min(box.w - 12, 160)}
4549
+ height={18}
4550
+ rx={pinnedGroup === box.key ? '5' : '4'}
4551
+ data-group-label-tint-rx={pinnedGroup === box.key ? '5' : '4'}
4415
4552
  fill={pinnedGroup === box.key || hoveredGroupLabel === box.key ? pal.legendAccent : 'transparent'}
4416
4553
  opacity={pinnedGroup === box.key ? (isLight ? 0.16 : 0.20)
4417
4554
  : hoveredGroupLabel === box.key ? (isLight ? 0.09 : 0.13)
4418
4555
  : 1}
4419
4556
  data-group-label-tinted={pinnedGroup === box.key ? 'pinned' : hoveredGroupLabel === box.key ? 'hover' : 'none'}
4420
- style={{ transition: 'fill 150ms ease-out, opacity 150ms ease-out' }}
4557
+ /* Round 459 / Loop cadence-sync follow-on to codex
4558
+ preview.125 (Hero D #147). Codex's parent <text>
4559
+ transition list now reads:
4560
+ 'fill 200ms, letter-spacing 200ms,
4561
+ font-weight 200ms, opacity 200ms'
4562
+ — 200ms ease-out across every axis. The label
4563
+ hitbox tint rect underneath was still at 150ms
4564
+ (legacy R107 cadence), so the tint snapped in
4565
+ 50ms ahead of the parent label brightening —
4566
+ a small but perceivable mistimed cascade when
4567
+ hovering or clicking to pin a cluster. R459
4568
+ lifts both axes to 200ms to lock the tint
4569
+ under the label as one motion-coherent state
4570
+ flip. Hover/pin/unpin all feel as a single
4571
+ unified ease rather than "tint pops, label
4572
+ follows". data-group-label-tint-transition
4573
+ attr exposes the timing for tests. */
4574
+ /* Round 460 / Loop — extend the R459-200ms tint rect
4575
+ transition list to include `x` + `width` so the
4576
+ hitbox slides into place when a cluster grows or
4577
+ shrinks (member joins / leaves / status change
4578
+ re-pricing box.w). Pre-R460 every resize snap-
4579
+ jumped the hitbox bounds — a small but visible
4580
+ glitch right at the moment the operator's
4581
+ attention is on the cluster. SVG2 CSS animation
4582
+ on geometry attrs has shipped in Chrome 95+ /
4583
+ Safari 16+ / FF 70+; the runtime gracefully
4584
+ no-ops on older browsers. Sibling motion idiom
4585
+ to R134 / R141 / R142 (panel rect transitions)
4586
+ at the group-label hitbox tier.
4587
+ data-group-label-tint-geom-transition attr
4588
+ exposes the geometry-axis presence for tests. */
4589
+ data-group-label-tint-transition="200ms"
4590
+ data-group-label-tint-geom-transition="x,width,rx"
4591
+ style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, x 200ms ease-out, width 200ms ease-out, rx 200ms ease-out' }}
4421
4592
  />
4422
4593
  {/* Round 218 / Loop: group label gains a letter-spacing
4423
4594
  transition on pin — the text subtly spaces out
@@ -4463,16 +4634,47 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4463
4634
  R432 group label text (this round)
4464
4635
  R218 transition list ('fill 200ms, letter-spacing
4465
4636
  200ms') untouched — additive conditional case. */}
4637
+ {/* Round 457 / Loop: group label parent text fontWeight
4638
+ 700 → 800 on isPinned. Adds typographic weight axis
4639
+ to the group-label parent text, sibling to R432
4640
+ letter-spacing tween at the same surface. Pre-R457
4641
+ pin lifted ls 0 → 0.5px (R218→R432 3-tier) but the
4642
+ fw stayed planted at R63's 700 — locked groups
4643
+ read as wider-but-same-weight. R457 adds the
4644
+ weight axis so pinned groups read as tightened
4645
+ AND wider, matching the R416/R424/R425/R426/R444/
4646
+ R445/R446 "data tightens under attention" idiom
4647
+ (now extended to the parent-text scope at the
4648
+ group-label tier). R63 fill brighten + R432
4649
+ letter-spacing 0/0.25/0.5 3-tier + R55 transition
4650
+ list all preserved; extends to include 'font-
4651
+ weight 200ms ease-out' so the bump eases under
4652
+ the same cadence. */}
4653
+ {/* v0.11.0 #147 Hero D — Vincent 5401 ask: "dash 网络
4654
+ 图里面这个工程的名字也太大了, 超级丑". Per Vincent
4655
+ screenshot 实测. Initial attempt moved label to
4656
+ bottom-right (#147 spec Option A); topo-overlap-test
4657
+ caught 7 grid collisions because cluster boxes have
4658
+ no bottom padding. Pivot to Option C: keep top-left
4659
+ anchor, shrink fontSize 13 → 9 (-31%, watermark
4660
+ register), dim default opacity 1 → 0.55 (hover/pin
4661
+ restore to 1). Net Twitter-grok improvement:
4662
+ cluster labels no longer dominate the canvas at
4663
+ rest; operator still hovers to find specific groups.
4664
+ Position unchanged to preserve the existing
4665
+ geometry that overlap-test gates. */}
4466
4666
  <text
4467
4667
  x={box.x + 12}
4468
- y={box.y + 14}
4668
+ y={box.y + 12}
4469
4669
  fill={isHovered ? pal.legendHeadline : pal.legendText}
4470
- fontSize="13"
4670
+ fontSize="9"
4471
4671
  fontFamily="monospace"
4472
- fontWeight="700"
4672
+ fontWeight={isPinned ? '800' : '700'}
4673
+ opacity={isPinned || isHovered ? 1 : 0.55}
4473
4674
  data-group-label-hovered={isHovered && !isPinned ? 'true' : 'false'}
4675
+ data-group-label-font-weight={isPinned ? '800' : '700'}
4474
4676
  style={{
4475
- transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
4677
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, opacity 200ms ease-out',
4476
4678
  letterSpacing: isPinned ? '0.5px' :
4477
4679
  isHovered ? '0.25px' : '0px',
4478
4680
  }}
@@ -4528,14 +4730,47 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4528
4730
  fill-inherit from parent label (hover-deepen-own-
4529
4731
  hue family) preserved. data-group-label-count-
4530
4732
  font-weight attr exposes the value for tests. */}
4733
+ {/* Round 444 / Loop: group label count tspan
4734
+ fontWeight 500 → 600 on isPinned. Extends the
4735
+ "data tightens under attention" typographic-
4736
+ weight pattern to a 5th anchor at the group-
4737
+ label-count scope:
4738
+ R416 chip-digit (chip hover)
4739
+ R424 panel-digit (panel hover)
4740
+ R425 hub-digit (hub hover)
4741
+ R426 edge-badge-digit (pin/hot)
4742
+ R444 group-label-count (pinned) ← this round
4743
+ Same idiom — when the group is locked, its
4744
+ member-count tightens typographically alongside
4745
+ the R432 letter-spacing spread (0 → 0.5px) on
4746
+ the parent label. Hover keeps rest fw (500) so
4747
+ the locked vs preview distinction at the type
4748
+ level stays intact — same gate R432 used.
4749
+ Monospace + R225 tabular-nums lock the digit
4750
+ width across fw changes; bbox unchanged; overlap-
4751
+ test invariants hold. transition list adds
4752
+ 'font-weight 200ms ease-out' matching R432
4753
+ letter-spacing cadence. R229 fill-inherit
4754
+ preserved (parent text fill still drives the
4755
+ hover/pin color). data-group-label-count-font-
4756
+ weight + -pinned attrs exposed for tests. */}
4757
+ {/* v0.11.0 #147 — count tspan tracks parent fontSize:
4758
+ 11 → 8 to match the new 9px label scale (parent
4759
+ dropped 13 → 9 with same -2px gap to the count
4760
+ suffix). dx="4" replaces dx="6" — the smaller
4761
+ glyph baseline doesn't need the wider gutter. */}
4531
4762
  <tspan
4532
- dx="6"
4533
- fontSize="11"
4534
- fontWeight="500"
4763
+ dx="4"
4764
+ fontSize="8"
4765
+ fontWeight={isPinned ? '600' : '500'}
4535
4766
  data-group-label-count={box.key}
4536
4767
  data-group-label-count-value={box.count}
4537
- data-group-label-count-font-weight="500"
4538
- style={{ fontVariantNumeric: 'tabular-nums' }}
4768
+ data-group-label-count-pinned={isPinned ? 'true' : 'false'}
4769
+ data-group-label-count-font-weight={isPinned ? '600' : '500'}
4770
+ style={{
4771
+ fontVariantNumeric: 'tabular-nums',
4772
+ transition: 'font-weight 200ms ease-out',
4773
+ }}
4539
4774
  >· {box.count}</tspan>
4540
4775
  {/* Round 58 / Loop: status mix pip strip. Compact text-
4541
4776
  based chips (e.g. "2w 1i") so the strip stays inside
@@ -4592,11 +4827,36 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4592
4827
  breakdown. Multi-tier groups (e.g. `alpha · 3
4593
4828
  2w 1i`) render unchanged — those pips genuinely
4594
4829
  add breakdown info that the total doesn't carry. */}
4830
+ {/* Round 458 / Loop — Hero D #147 finishing polish on top of
4831
+ N站牛/codex preview.125 (Option C: top-left label fontSize
4832
+ 13→9 + opacity 0.55 rest / 1 hover+pin, count tspan 11→8).
4833
+ That ship left the 3 status pips at fontSize=11 — visibly
4834
+ DOMINATING the now-9px parent label they trail. Result on
4835
+ a 5-member cluster: `alpha · 5 3w 2i` renders inside-out
4836
+ as "tiny name + tiny count + BIG bright pips" rather than
4837
+ a coherent right-tail of metadata. R458 scales the 3 pips
4838
+ to fontSize=8 (matches count tspan) and tightens dx 8/4/4
4839
+ → 6/3/3 (gutter ratio 0.73/0.36 glyph-widths @ 11px ≈
4840
+ 0.75/0.38 glyph-widths @ 8px — same visual rhythm at the
4841
+ smaller scale). The whole group-label bottom-right strip
4842
+ now reads as a unified 9/8/8/8 typographic ladder:
4843
+ name (parent <text>) fontSize 9 fw 700/800
4844
+ · count (1st tspan) fontSize 8 fw 500/600
4845
+ Nw (2nd tspan) fontSize 8 fw 600
4846
+ Ni (3rd tspan) fontSize 8 fw 600
4847
+ No (4th tspan) fontSize 8 fw 600
4848
+ Closes Vincent /goal 5401 ("太大太丑") at the pip-strip
4849
+ tier; with codex preview.125 the spec is fully realized.
4850
+ Geometry-only attribute changes — bbox tightens slightly
4851
+ (8px chars vs 11px chars stay inside the original 240px
4852
+ hitbox max) so topo-overlap-test invariants hold.
4853
+ tabular-nums + anet-fade-in + theme-eased fill 200ms
4854
+ preserved on every tspan. */}
4595
4855
  {box.statuses.working > 0 && box.statuses.working !== box.count && (
4596
4856
  <tspan
4597
- dx="8"
4857
+ dx="6"
4598
4858
  fill={isLight ? '#059669' : '#22c55e'}
4599
- fontSize="11"
4859
+ fontSize="8"
4600
4860
  fontWeight="600"
4601
4861
  className="anet-fade-in"
4602
4862
  data-group-pip="working"
@@ -4605,9 +4865,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4605
4865
  )}
4606
4866
  {box.statuses.idle > 0 && box.statuses.idle !== box.count && (
4607
4867
  <tspan
4608
- dx="4"
4868
+ dx="3"
4609
4869
  fill={isLight ? '#0d9488' : '#2dd4bf'}
4610
- fontSize="11"
4870
+ fontSize="8"
4611
4871
  fontWeight="600"
4612
4872
  className="anet-fade-in"
4613
4873
  data-group-pip="idle"
@@ -4616,9 +4876,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4616
4876
  )}
4617
4877
  {box.statuses.offline > 0 && box.statuses.offline !== box.count && (
4618
4878
  <tspan
4619
- dx="4"
4879
+ dx="3"
4620
4880
  fill={isLight ? '#94a3b8' : '#6b7280'}
4621
- fontSize="11"
4881
+ fontSize="8"
4622
4882
  fontWeight="600"
4623
4883
  className="anet-fade-in"
4624
4884
  data-group-pip="offline"
@@ -4972,25 +5232,29 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4972
5232
  all preserved. data-edge-particle-radius attr
4973
5233
  exposes the value for tests. */
4974
5234
  <circle
4975
- r="4.5"
5235
+ /* Round 439 / Loop: edge flow particle radius hover
5236
+ lift — r 4.5 → 5.5 on (isHoveredEdge ||
5237
+ isEndpointHoveredEdge). Continues edge paint-
5238
+ layer parity arc (R436 visible path sw / R437
5239
+ flow-rail sw / R439 particle r) so the whole
5240
+ edge surface — including the moving particle —
5241
+ lifts on hover, not just the static stripes.
5242
+ +1px radius gives ~50% area boost. Subtler than
5243
+ 1.4× sw bump on visible path because the
5244
+ particle is already small + motion-bright;
5245
+ +1px reads as "the dot caught attention"
5246
+ without overshadowing the path lift. R252
5247
+ transition list extends to include r 200ms so
5248
+ the size change eases under the same fill/
5249
+ opacity cadence. */
5250
+ r={(isHoveredEdge || isEndpointHoveredEdge) ? 5.5 : 4.5}
4976
5251
  fill={pal.flowParticle}
4977
5252
  filter={isLight ? undefined : 'url(#topo-glow)'}
4978
5253
  opacity={Math.min(1, fresh * edgeOpacityMul)}
4979
5254
  data-edge-particle={link.key}
4980
- data-edge-particle-radius="4.5"
4981
- /* Round 252 / Loop: particle picks up fill +
4982
- opacity transition for theme-toggle smoothing.
4983
- Pre-R252 pal.flowParticle (cyber #fef08a yellow
4984
- ↔ light #f59e0b amber) snapped on theme toggle
4985
- while every other edge element eased (R245
4986
- paths, R251 badge, R242 chat-target, R233
4987
- endpoint ring). opacity is freshness-driven so
4988
- it transitions per-frame as fresh decays anyway
4989
- — but adding opacity to the explicit transition
4990
- list also covers theme toggle (R3 className
4991
- transition-opacity duration-300 was previously
4992
- absent on this circle). */
4993
- style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out' }}
5255
+ data-edge-particle-radius={(isHoveredEdge || isEndpointHoveredEdge) ? 5.5 : 4.5}
5256
+ data-edge-particle-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5257
+ style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, r 200ms ease-out' }}
4994
5258
  >
4995
5259
  <animateMotion
4996
5260
  dur={`${duration}s`}
@@ -5722,13 +5986,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5722
5986
  // Mode='spline' + R245 ease-in-out keySplines all
5723
5987
  // preserved. data-topo-hub-halo-radius attr exposes
5724
5988
  // value for tests.
5989
+ /* Round 451 / Loop: hub center halo radius lift on
5990
+ hoveredHub — r 20 → 22 (+2px, ~21% area). Adds another
5991
+ geometric axis to the hub-hover signature stack
5992
+ alongside R177 ring radius lift + R209 digit scale +
5993
+ R425 digit fw + R370 halo opacity + R386 highlight
5994
+ opacity + R441 core fill chroma. Pre-R451 the halo
5995
+ r stayed planted at R408's 20px while the rest of
5996
+ the hub structure responded to hover. R451 makes
5997
+ the halo breath outward on hover so the focal pulse
5998
+ intensifies under attention. SMIL `<animate>` on
5999
+ opacity continues independently (animateAttr=
6000
+ 'opacity' vs CSS-property r — non-conflicting). R408
6001
+ base radius 20 preserved as rest; +2 hover delta
6002
+ keeps clearance from the R177 hub-hover-ring at
6003
+ r=17 hover (halo is BEHIND the ring, halo r=22 sits
6004
+ 5px beyond the ring's hover-r=17, still well within
6005
+ the hub canvas envelope). data-topo-hub-halo-radius
6006
+ attr now reports the dynamic value. */
6007
+ const isHaloHovered = !reducedMotion && hoveredHub;
6008
+ const haloR = isHaloHovered ? 22 : 20;
5725
6009
  return (
5726
6010
  <circle
5727
- cx={cx} cy={cy} r="20"
6011
+ cx={cx} cy={cy}
5728
6012
  fill={isLight ? '#d1fae5' : '#10b981'}
5729
6013
  opacity={isLight ? 0.42 : 0.12}
5730
6014
  data-hub-busyness={busy}
5731
- data-topo-hub-halo-radius="20"
6015
+ data-topo-hub-halo-radius={haloR}
6016
+ data-topo-hub-halo-hovered={isHaloHovered ? 'true' : 'false'}
5732
6017
  data-topo-hub-halo-trough={isLight ? troughLight : troughDark}
5733
6018
  data-topo-hub-halo-peak={isLight ? peakLight : peakDark}
5734
6019
  /* Round 253 / Loop: hub grounding halo fill transition
@@ -5737,8 +6022,14 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5737
6022
  animate on opacity continued running. CSS fill
5738
6023
  transition is independent of the SMIL animate
5739
6024
  (different attributes), so they compose without
5740
- conflict. */
5741
- style={{ transition: 'fill 200ms ease-out' }}
6025
+ conflict.
6026
+ R451: r as CSS property (R197/R198 idiom) so the
6027
+ hover-radius tween eases smoothly under the same
6028
+ 200ms cadence as fill. */
6029
+ style={{
6030
+ r: `${haloR}px`,
6031
+ transition: 'fill 200ms ease-out, r 200ms ease-out',
6032
+ } as React.CSSProperties}
5742
6033
  >
5743
6034
  {/* Round 244 / Loop: hub grounding halo breath gets
5744
6035
  ease-in-out keySplines, matching the active-node
@@ -5783,12 +6074,45 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5783
6074
  attr added for test introspection (the parent <g> at
5784
6075
  line 3587 has data-topo-hub but the core specifically
5785
6076
  is the canvas anchor). */}
5786
- <circle
5787
- cx={cx} cy={cy} r="10"
5788
- fill={isLight ? '#059669' : '#10b981'}
5789
- data-topo-hub-core
5790
- style={{ transition: 'fill 200ms ease-out' }}
5791
- />
6077
+ {(() => {
6078
+ /* Round 441 / Loop: hub center core fill brighten on
6079
+ hoveredHub. Pre-R441 the core was static (cyber
6080
+ emerald-500 #10b981 / light emerald-600 #059669) and
6081
+ the hub-hover gesture lifted ring radius (R177) +
6082
+ digit scale (R209) + digit fw (R425) + halo opacity
6083
+ (R370) + highlight opacity (R386) but the focal core
6084
+ ITSELF stayed planted at rest tone. R441 shifts the
6085
+ fill one emerald tier brighter on hover so the canvas
6086
+ anchor itself responds:
6087
+ cyber emerald-500 → emerald-400 (#10b981 → #34d399)
6088
+ light emerald-600 → emerald-500 (#059669 → #10b981)
6089
+ Same +100 step on the emerald scale across both themes.
6090
+ Pure paint axis; no geometry change. R248 fill 200ms
6091
+ transition already in the style list eases the shift.
6092
+ Closes the chroma axis on the hub-hover gesture stack:
6093
+ R177 ring radius lift geometry
6094
+ R209 digit scale 1.08 geometry
6095
+ R425 digit fw 700 → 800 typography
6096
+ R370 halo opacity 0.7 → 0.8 paint
6097
+ R386 highlight opacity paint
6098
+ R441 core fill brighten chroma ← this round
6099
+ data-topo-hub-core-hovered + -fill attrs exposed
6100
+ for tests. */
6101
+ const isCoreHovered = !reducedMotion && hoveredHub;
6102
+ const coreFill = isLight
6103
+ ? (isCoreHovered ? '#10b981' : '#059669')
6104
+ : (isCoreHovered ? '#34d399' : '#10b981');
6105
+ return (
6106
+ <circle
6107
+ cx={cx} cy={cy} r="10"
6108
+ fill={coreFill}
6109
+ data-topo-hub-core
6110
+ data-topo-hub-core-hovered={isCoreHovered ? 'true' : 'false'}
6111
+ data-topo-hub-core-fill={coreFill}
6112
+ style={{ transition: 'fill 200ms ease-out' }}
6113
+ />
6114
+ );
6115
+ })()}
5792
6116
  {/* R130 / Loop: when workingCount > 0, the decorative inner
5793
6117
  highlight gets replaced with the workingCount digit. The
5794
6118
  R84 busyness breath already encodes the same metric
@@ -6570,13 +6894,77 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6570
6894
  tion list ('fill,opacity' 300ms ease-out) unchanged.
6571
6895
  data-node-halo-offline-opacity attr exposes the
6572
6896
  resolved value for tests. */}
6897
+ {(() => {
6898
+ /* Round 440 / Loop: node halo opacity hover lift —
6899
+ lifts toward full on the matched node. Pure paint
6900
+ axis: rest values unchanged for un-hovered halos,
6901
+ hover state lifts the matched halo's alpha by
6902
+ +0.15 on each tier:
6903
+ online cyber 0.65 → 0.80
6904
+ online light 0.85 → 1.00 (capped)
6905
+ offline cyber 0.30 → 0.45
6906
+ offline light 0.45 → 0.60
6907
+ Same paint-only mental model as R430 hub-spoke
6908
+ opacity lift + R429 label-card body opacity lift,
6909
+ now at the per-node halo scope. No geometry
6910
+ change so R51 sentinels stay safe and the overlap-
6911
+ test invariant is unchanged (test runs at rest).
6912
+ Closes a chroma/presence axis on the per-node
6913
+ hover signature alongside the 12-layer cue stack
6914
+ (R26/R217/R142/R427/R428/R429 card + R430/R435/
6915
+ R436/R437/R94 link + R438 ring). R407 offline
6916
+ halo opacity floor (cyber 0.30 / light 0.45) is
6917
+ the rest branch unchanged. Existing transition-
6918
+ [fill,opacity] duration-300 className handles
6919
+ the easing. data-node-halo-hovered exposes the
6920
+ gate; data-node-halo-resolved-opacity exposes
6921
+ the four-state resolved value for tests. */
6922
+ const isHaloHovered = !reducedMotion && hoveredAlias === session.alias;
6923
+ /* Round 456 / Loop: light-theme offline node halo
6924
+ rest opacity 0.45 → 0.50. Stale-state legibility
6925
+ lift family extension (10th anchor) at the per-
6926
+ node halo light-theme scope:
6927
+ R317 subordinate-text gray-500 → gray-400
6928
+ R358 freshness floor 0.25 → 0.30
6929
+ R372 minimap offline-dot 0.5 → 0.6
6930
+ R404 hub-halo cyber trough 0.08 → 0.10
6931
+ R405 hub-halo light trough 0.32 → 0.34
6932
+ R406 edge freshness floor 0.35 → 0.40
6933
+ R407 node halo offline opacity
6934
+ cyber 0.25 → 0.30
6935
+ light 0.4 → 0.45
6936
+ R419 hub-spoke idle 0.45 → 0.50
6937
+ R452 dense alias rest 0.9 → 0.95
6938
+ R456 node halo offline LIGHT 0.45 → 0.50 ← this round
6939
+ Pre-R456 light-theme offline halo at 0.45 sat at
6940
+ the upper end of "near-floor" but read as soft-
6941
+ focus on the lighter canvas; +0.05 (~11 % opacity
6942
+ gain) lifts it to 0.50 — the midpoint between
6943
+ R407 rest 0.45 and R440 hover 0.60 — closing the
6944
+ gap so offline halos read more confidently as
6945
+ present-but-stale anchors. Cyber theme stays at
6946
+ R407's 0.30 (cyber backdrop is dark; the cyber
6947
+ offline halo against #080814 contains a stronger
6948
+ contrast envelope than light, so doesn't need
6949
+ the same lift). R440 hover 0.45→0.60 light + R12
6950
+ status.halo color + R407 transition list all
6951
+ preserved. */
6952
+ const haloOpacity = (() => {
6953
+ if (isOnline) {
6954
+ return isLight ? (isHaloHovered ? 1 : 0.85) : (isHaloHovered ? 0.80 : 0.65);
6955
+ }
6956
+ return isLight ? (isHaloHovered ? 0.60 : 0.50) : (isHaloHovered ? 0.45 : 0.30);
6957
+ })();
6958
+ return (
6573
6959
  <circle
6574
6960
  cx={pos.x}
6575
6961
  cy={pos.y}
6576
6962
  r={radius + 8}
6577
6963
  fill={status.halo}
6578
- opacity={isOnline ? (isLight ? 0.85 : 0.65) : (isLight ? 0.45 : 0.30)}
6964
+ opacity={haloOpacity}
6579
6965
  data-node-halo-offline-opacity={isOnline ? undefined : (isLight ? 0.45 : 0.30)}
6966
+ data-node-halo-hovered={isHaloHovered ? 'true' : 'false'}
6967
+ data-node-halo-resolved-opacity={haloOpacity}
6580
6968
  className="transition-[fill,opacity] duration-300 ease-out"
6581
6969
  data-node-halo-breath={!reducedMotion && session.status === 'working' ? 'on' : 'off'}
6582
6970
  data-node-halo-breath-offset={
@@ -6635,6 +7023,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6635
7023
  />
6636
7024
  )}
6637
7025
  </circle>
7026
+ );
7027
+ })()}
6638
7028
  {/* Round 111 / Loop: edge-endpoint emphasis ring. R49
6639
7029
  already keeps endpoint nodes at opacity 1 while
6640
7030
  others dim when an edge is hovered, but the
@@ -6691,11 +7081,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6691
7081
  ring stroke-width). data-edge-endpoint-ring-
6692
7082
  stroke-width attr surfaces the chosen value for
6693
7083
  test introspection. */
7084
+ /* Round 442 / Loop: endpoint emphasis ring radius
7085
+ hover lift — r=radius+7 → radius+8 on isEndpoint,
7086
+ closing a 3-axis hover-elevation parity at endpoint
7087
+ ring scope (r + sw + opacity):
7088
+ opacity R182 0 → 0.85/0.9
7089
+ sw R233 1.6 → 2.4
7090
+ r R442 +7 → +8 ← this round
7091
+ Mirrors the 3-axis trios already established at
7092
+ hub hover-ring (R177/R370/R385) and edge badge
7093
+ (R164/R394/R395). Pre-R442 the endpoint ring
7094
+ faded in + thickened on edge-hover but its radius
7095
+ stayed locked at radius+7 — only the paint/weight
7096
+ axes lifted while the GEOMETRY stayed unchanged.
7097
+ +1px (~radius+7 to radius+8) gives a subtle outward
7098
+ pulse on hover without crowding the status ring
7099
+ (which sits at radius from R438 sw3.5 hover) or
7100
+ the halo (radius+8 from R440 opacity hover — the
7101
+ endpoint ring sits at the SAME radius as the halo
7102
+ but with stroke=cyan vs fill=status.halo so they
7103
+ don't visually collide). The transition list
7104
+ extends to include 'r 180ms ease-out' so the new
7105
+ axis eases under the same R233 cadence. SVG `r`
7106
+ on a <circle> uses CSS-property syntax for inter-
7107
+ polation (same idiom R197/R198 used on the
7108
+ legend swatch). data-edge-endpoint-ring-radius
7109
+ attr exposes the resolved value for tests. */
7110
+ const endpointR = isEndpoint ? radius + 8 : radius + 7;
6694
7111
  return (
6695
7112
  <circle
6696
7113
  cx={pos.x}
6697
7114
  cy={pos.y}
6698
- r={radius + 7}
6699
7115
  fill="none"
6700
7116
  stroke={pal.flowEdge}
6701
7117
  strokeWidth={isEndpoint ? 2.4 : 1.6}
@@ -6703,7 +7119,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6703
7119
  data-edge-endpoint-ring
6704
7120
  data-edge-endpoint-active={isEndpoint ? 'true' : 'false'}
6705
7121
  data-edge-endpoint-ring-stroke-width={isEndpoint ? 2.4 : 1.6}
6706
- style={{ pointerEvents: 'none', transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out' }}
7122
+ data-edge-endpoint-ring-radius={endpointR}
7123
+ style={{
7124
+ pointerEvents: 'none',
7125
+ r: `${endpointR}px`,
7126
+ transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out, r 180ms ease-out',
7127
+ } as React.CSSProperties}
6707
7128
  />
6708
7129
  );
6709
7130
  })()}
@@ -6963,8 +7384,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6963
7384
  transition: 'r 150ms ease-out, stroke-width 150ms ease-out',
6964
7385
  } as React.CSSProperties}
6965
7386
  />
7387
+ {/* Round 443 / Loop: runtime badge inner-icon
7388
+ strokeWidth lift on node hover — 2.4 → 2.8 on
7389
+ isNodeActive. Pre-R443 the outer badge ring
7390
+ lifted (R208 r + sw both grow on hover) but
7391
+ the inner icon path stayed locked at sw=2.4.
7392
+ The two layers of the runtime badge were
7393
+ out of phase: ring thickened, icon stayed
7394
+ thin. R443 closes the 2-axis hover signature
7395
+ on the badge so both ring and icon lift
7396
+ together. +0.4 absolute delta matches the
7397
+ R208 ring's +0.5 sw delta (badge ring 1.5 →
7398
+ 2.0 absolute), proportional to the icon's
7399
+ heavier base of 2.4. Pure paint axis;
7400
+ strokeLinecap='round' + strokeLinejoin='round'
7401
+ preserved. transition list extends to include
7402
+ 'stroke-width 150ms ease-out' matching R208
7403
+ outer-ring cadence. data-runtime-badge-icon
7404
+ + -active attrs exposed for tests. */}
6966
7405
  <g transform={`translate(${bx - icon / 2} ${by - icon / 2}) scale(${icon / 24})`}>
6967
- <path d={rt.iconPath} fill="none" stroke={rt.color} strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
7406
+ <path
7407
+ d={rt.iconPath}
7408
+ fill="none"
7409
+ stroke={rt.color}
7410
+ strokeWidth={isNodeActive ? '2.8' : '2.4'}
7411
+ strokeLinecap="round"
7412
+ strokeLinejoin="round"
7413
+ data-runtime-badge-icon={session.alias}
7414
+ data-runtime-badge-icon-active={isNodeActive ? 'true' : 'false'}
7415
+ data-runtime-badge-icon-stroke-width={isNodeActive ? '2.8' : '2.4'}
7416
+ style={{ transition: 'stroke-width 150ms ease-out' }}
7417
+ />
6968
7418
  </g>
6969
7419
  </g>
6970
7420
  );
@@ -7269,12 +7719,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7269
7719
  hover scope. R211 fill 300ms transition
7270
7720
  preserved (additive letter-spacing branch
7271
7721
  + appended 'letter-spacing 200ms ease-out'). */}
7722
+ {/* Round 448 / Loop: node sub-text fontWeight
7723
+ 400 → 500 (font-medium). Sibling to R363
7724
+ (recent-row text fw 400→500) + R364 (legend-
7725
+ row label fw 400→500) — same "small mono
7726
+ text at fontSize=9-11 needs 500-tier weight
7727
+ for legibility" pattern, now applied to the
7728
+ per-node sub-text line. At fontSize=8-9
7729
+ monospace against the label-card chrome
7730
+ (pal.labelBox.fill cyber #020617 / light
7731
+ #ffffff), the default fw=400 sits at the
7732
+ legibility floor; fw=500 (font-medium) lifts
7733
+ it into a clearly readable band without
7734
+ changing geometry. R211 fill 300ms +
7735
+ R428 letter-spacing 0→0.2 hover + R427
7736
+ alias-text + R429 body opacity all preserved.
7737
+ Pure typography lift; no layout shift; the
7738
+ alias-text fw=700 (R427) still wins so the
7739
+ alias > status hierarchy holds at the type
7740
+ level. data-node-sub-text-font-weight attr
7741
+ exposes the value for tests. */}
7272
7742
  <text
7273
7743
  x="0" y={subY} textAnchor="middle"
7274
7744
  fill={status.primary}
7275
7745
  fontSize={subFs} fontFamily="monospace"
7746
+ fontWeight="500"
7276
7747
  data-node-sub-text={session.alias}
7277
7748
  data-node-sub-text-hovered={hoveredAlias === session.alias ? 'true' : 'false'}
7749
+ data-node-sub-text-font-weight="500"
7278
7750
  style={{
7279
7751
  transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',
7280
7752
  letterSpacing: hoveredAlias === session.alias ? '0.2px' : '0px',
@@ -7301,6 +7773,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7301
7773
  // CSS pseudo-class; only the transition-property
7302
7774
  // moves to inline. Big fleets benefit most — this is
7303
7775
  // the path users see when their dashboard is busiest.
7776
+ /* Round 452 / Loop: dense plain-text alias rest
7777
+ opacity 0.9 → 0.95. Closes the alpha gap on the
7778
+ dense fleet's per-node label, sibling to R449
7779
+ legend-count-active 0.95→1.0 and R450 minimap
7780
+ viewport rest 0.9→0.95 — same "close the
7781
+ active-presence alpha gap" idiom applied here
7782
+ to the dense-mode alias text at fontSize=9-10
7783
+ monospace. Pre-R452 dense aliases at α=0.9 sat
7784
+ just below full alpha; for un-hovered nodes in
7785
+ a busy >16-node fleet this is the only label
7786
+ readable, so the 10% alpha gap added a subtle
7787
+ "soft-focused chrome" feel where the labels
7788
+ should read as definitive. +0.05 lift makes
7789
+ them confidently present without erasing the
7790
+ status.text + R110 stroke halo + paintOrder
7791
+ layering. R26 group-hover translate + R212
7792
+ fill 300ms transition + R110 stroke=container-
7793
+ Bg halo all preserved. data-node-dense-alias-
7794
+ text-opacity attr exposes the resolved value
7795
+ for tests. */
7304
7796
  <text
7305
7797
  x={pos.x}
7306
7798
  y={pos.y + radius + denseDrop}
@@ -7309,9 +7801,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7309
7801
  fontSize={denseFs}
7310
7802
  fontFamily="monospace"
7311
7803
  fontWeight="700"
7312
- opacity={0.9}
7804
+ opacity={0.95}
7313
7805
  className="group-hover:-translate-y-[1.5px]"
7314
7806
  data-node-dense-alias-text={session.alias}
7807
+ data-node-dense-alias-text-opacity="0.95"
7315
7808
  style={{
7316
7809
  pointerEvents: 'none',
7317
7810
  paintOrder: 'stroke',
@@ -8333,13 +8826,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8333
8826
  R383 recent-row pip radius 1.8 → 2.0 (this round)
8334
8827
  data-recent-row-freshness-radius attr
8335
8828
  bumps to '2.0' for tests. */
8336
- r={2.0}
8829
+ /* Round 447 / Loop: recent-row freshness pip
8830
+ radius lift on (isRowHovered || isRowPinned)
8831
+ — r 2.0 → 2.5 (+0.5px, sibling to R442
8832
+ endpoint-ring r lift). Adds a geometric
8833
+ axis to the recent-row hover/pin gesture
8834
+ alongside R143 translateY + R104 row bg-
8835
+ tint + R434 letter-spacing + R445 count
8836
+ fw. Pre-R447 the pip stayed at r=2.0 always
8837
+ — the freshness alpha (R162) tracked
8838
+ recency but didn't telegraph "this row is
8839
+ in focus" geometrically. R447 lifts the
8840
+ pip outward by 25% area (π·2.5² / π·2.0²
8841
+ = 1.56) on attention, closing a 5-axis
8842
+ row-attention signature (geometry + paint
8843
+ + typography + spacing + position).
8844
+ SVG `r` as CSS property for interpolation
8845
+ (R197/R198 idiom). transition list extends
8846
+ to include 'r 200ms ease-out' matching the
8847
+ opacity cadence. data-recent-row-freshness-
8848
+ lifted attr exposes the gate for tests. */
8337
8849
  fill={pal.legendAccent}
8338
8850
  opacity={alpha}
8339
8851
  data-recent-row-freshness={link.key}
8340
8852
  data-recent-row-freshness-alpha={alpha.toFixed(2)}
8341
- data-recent-row-freshness-radius="2.0"
8342
- style={{ pointerEvents: 'none', transition: 'opacity 200ms ease-out' }}
8853
+ data-recent-row-freshness-radius={(isRowHovered || isRowPinned) ? 2.5 : 2.0}
8854
+ data-recent-row-freshness-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}
8855
+ style={{
8856
+ pointerEvents: 'none',
8857
+ r: `${(isRowHovered || isRowPinned) ? 2.5 : 2.0}px`,
8858
+ transition: 'opacity 200ms ease-out, r 200ms ease-out',
8859
+ } as React.CSSProperties}
8343
8860
  />
8344
8861
  );
8345
8862
  })()}
@@ -8486,13 +9003,36 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8486
9003
  distinct, plus the fill flip from legendText
8487
9004
  → amber carries the dramatic part of the cue).
8488
9005
  Sibling treatment in the data-weight tier. */}
9006
+ {/* Round 445 / Loop: extend the R320 cold/hot fw
9007
+ binary (600/700) to ALSO fire on isRowPinned —
9008
+ pinned-cold now lifts to 700 alongside the
9009
+ existing hot-triggered lift. Sibling to R444
9010
+ group-label-count-pin (500→600) at the
9011
+ recent-row scope. Both panel-row counts now
9012
+ respond to pin with a typographic weight lift,
9013
+ part of the "data tightens under attention"
9014
+ family (R416/R424/R425/R426/R444/R445).
9015
+ Effective tiers:
9016
+ cold + un-pinned → fw 600
9017
+ cold + pinned → fw 700 ← this round
9018
+ hot (any pin state) → fw 700 (R320 preserved)
9019
+ hot is still amber-filled (R127); cold pin
9020
+ stays at the parent fill, so the two routes
9021
+ to fw=700 are visually distinct (color vs
9022
+ no color). transition list adds 'font-
9023
+ weight 200ms ease-out' so the lift eases
9024
+ under the same R320 fill cadence. data-
9025
+ recent-row-count-pinned attr exposes the
9026
+ pin gate for tests. */}
8489
9027
  <tspan
8490
9028
  fill={isHot ? hotStroke : undefined}
8491
- fontWeight={isHot ? '700' : '600'}
9029
+ fontWeight={(isHot || isRowPinned) ? '700' : '600'}
8492
9030
  data-recent-row-count
9031
+ data-recent-row-count-pinned={isRowPinned ? 'true' : 'false'}
9032
+ data-recent-row-count-font-weight={(isHot || isRowPinned) ? '700' : '600'}
8493
9033
  {...(isHot ? { 'data-recent-row-count-hot': 'true' } : {})}
8494
9034
  style={{
8495
- transition: 'fill 300ms ease-out',
9035
+ transition: 'fill 300ms ease-out, font-weight 200ms ease-out',
8496
9036
  fontVariantNumeric: 'tabular-nums',
8497
9037
  }}
8498
9038
  >
@@ -9318,20 +9858,60 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9318
9858
  labels to single status words, the count
9319
9859
  becomes proportionally more important; this
9320
9860
  round emphasizes that role typographically. */}
9861
+ {/* Round 446 / Loop: legend per-row count fontWeight
9862
+ lift 600 → 700 on isPinned. Mirror of R444 group-
9863
+ label-count + R445 recent-row-count at the
9864
+ legend-row scope. Closes the 3-panel-row family
9865
+ for the "data tightens under attention" pattern —
9866
+ every panel-row count now responds to pin with a
9867
+ typographic-weight bump:
9868
+ R444 group-label-count 500 → 600
9869
+ R445 recent-row-count 600 → 700 (cold-pin route)
9870
+ R446 legend-row-count 600 → 700 ← this round
9871
+ Hover gate (hoveredStatus===row.key) keeps rest
9872
+ fw=600 so the locked-vs-preview distinction at
9873
+ the type level stays intact — same gate R433 used
9874
+ on the parent <text> letter-spacing tween. R309
9875
+ fw=600 baseline + R204 empty-row opacity dim +
9876
+ R225 tabular-nums all preserved. transition list
9877
+ extends to include 'font-weight 150ms ease-out'
9878
+ matching R433 fill/letter-spacing cadence.
9879
+ data-legend-count-pinned + -font-weight attrs
9880
+ exposed for tests. */}
9321
9881
  <text
9322
9882
  x="215" y={row.y1}
9323
9883
  textAnchor="end"
9324
9884
  fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? row.fill : pal.legendText}
9325
9885
  fontSize="11"
9326
9886
  fontFamily="monospace"
9327
- fontWeight="600"
9887
+ fontWeight={isPinned ? '700' : '600'}
9888
+ /* Round 449 / Loop: legend-row count active-state
9889
+ opacity 0.95 → 1.0 on (hoveredStatus===row.key
9890
+ || isPinned). Pre-R449 R204 lifted populated-row
9891
+ active opacity from rest 0.65 to 0.95 — visibly
9892
+ brighter but kept a 5 pct alpha gap (1 - 0.95).
9893
+ R449 closes the gap to 1.0 so the active count
9894
+ reads as confidently present alongside the R446
9895
+ fw=600→700 + R433 letter-spacing tween. Theme-
9896
+ consistency / canvas-presence family extension
9897
+ (7th anchor on the active-presence lift sub-
9898
+ family): R370 hub hover-ring 0.7→0.8, R371 edge-
9899
+ badge rest 0.82→0.85, R372 minimap offline-dot
9900
+ 0.5→0.6, R386 hub-highlight idle 0.9→0.95, R387
9901
+ hover-detail panel 0.94→0.97, R429 label-card
9902
+ body 0.94→1.0, R449 legend-count active 0.95→1.0
9903
+ ← this round. Empty-row opacity (R204: 0.28
9904
+ light / 0.30 cyber) and idle 0.65 rest both
9905
+ preserved. */
9328
9906
  opacity={row.count === 0
9329
9907
  ? (isLight ? 0.28 : 0.30)
9330
- : (hoveredStatus === row.key || isPinned ? 0.95 : 0.65)}
9908
+ : (hoveredStatus === row.key || isPinned ? 1 : 0.65)}
9331
9909
  data-legend-count={row.key}
9332
9910
  data-legend-count-empty={row.count === 0 ? 'true' : 'false'}
9911
+ data-legend-count-pinned={isPinned ? 'true' : 'false'}
9912
+ data-legend-count-font-weight={isPinned ? '700' : '600'}
9333
9913
  data-legend-count-fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? 'tier' : 'neutral'}
9334
- style={{ pointerEvents: 'none', transition: 'opacity 150ms ease-out, fill 150ms ease-out', fontVariantNumeric: 'tabular-nums' }}
9914
+ style={{ pointerEvents: 'none', transition: 'opacity 150ms ease-out, fill 150ms ease-out, font-weight 150ms ease-out', fontVariantNumeric: 'tabular-nums' }}
9335
9915
  >{row.count}</text>
9336
9916
  </g>
9337
9917
  );
@@ -9718,7 +10298,29 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9718
10298
  // R346: strokeWidth + opacity tween on container hover.
9719
10299
  strokeWidth={hoveredMinimap ? '1.75' : '1.5'}
9720
10300
  strokeLinejoin="round"
9721
- opacity={hoveredMinimap ? '1' : '0.9'}
10301
+ /* Round 450 / Loop · milestone: minimap viewport rest
10302
+ opacity 0.9 → 0.95. Closes half the alpha gap on
10303
+ the wayfinding indicator while preserving the
10304
+ R346 hover delta to 1.0. Pre-R450 the rest viewport
10305
+ sat at 0.9 (10 pct alpha gap) — adequate but
10306
+ under-confident for the user's primary "you are
10307
+ here" indicator on the minimap. R450 lifts to 0.95
10308
+ so the rest read is more present without erasing
10309
+ the hover lift cue (the +0.05 rest-to-hover delta
10310
+ is small but pairs with R346 sw 1.5→1.75 to keep
10311
+ hover clearly distinguishable). Sibling to R449
10312
+ legend-count active opacity 0.95→1.0 — same
10313
+ "close the active-presence alpha gap" idiom now
10314
+ applied to the REST tier of the wayfinding rect
10315
+ (the minimap viewport stays at canvas-presence
10316
+ register even when un-hovered since it's the
10317
+ spatial referent). Theme-consistency / canvas-
10318
+ presence family (8th anchor on the active-
10319
+ presence lift sub-arc).
10320
+ R287 strokeWidth=1.5 + R379 strokeLinejoin='round'
10321
+ + R346 hover-state tweens + R393 rx=2 + R199
10322
+ smoothView x/y/w/h transition all preserved. */
10323
+ opacity={hoveredMinimap ? '1' : '0.95'}
9722
10324
  data-topo-minimap-viewport
9723
10325
  data-topo-minimap-viewport-rx="2"
9724
10326
  data-topo-minimap-viewport-smooth={smoothView ? 'true' : 'false'}
@@ -9921,11 +10523,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9921
10523
  its own compositor layer for crisper edges during
9922
10524
  the scale tween. Sibling change on zoom-in icon
9923
10525
  below. */}
10526
+ {/* Round 454 / Loop: extend R453 chrome reset icon hover
10527
+ sw lift to zoom +/− icons via Tailwind arbitrary class
10528
+ group-hover:[stroke-width:2.8]. Chrome icon hover sw
10529
+ lift family now 5 anchors:
10530
+ R208 runtime badge outer ring 1.5 → 2
10531
+ R443 runtime badge inner icon 2.4 → 2.8
10532
+ R453 chrome reset icon 2.5 → 2.8
10533
+ R454 chrome zoom-out icon 2.5 → 2.8 ← this round
10534
+ R454 chrome zoom-in icon 2.5 → 2.8 ← this round
10535
+ Tailwind v4 arbitrary-value group-hover variant
10536
+ resolves [stroke-width:2.8] as a CSS property which
10537
+ overrides the static strokeWidth='2.5' attribute on
10538
+ hover. transition-[stroke-width] appended to the
10539
+ existing transition-transform list so the sw tween
10540
+ eases under the same 200ms cadence as R352 group-
10541
+ hover:scale-110. R186 anet-chrome-pop keyframe still
10542
+ owns transform during click via CSS-animation
10543
+ precedence over transition-transform. Sibling change
10544
+ on zoom-in icon below. */}
9924
10545
  <svg
9925
10546
  width="12" height="12" viewBox="0 0 24 24"
9926
10547
  fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
9927
10548
  aria-hidden
9928
- className={`transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu${chromePopping === 'zoom-out' ? ' anet-chrome-pop' : ''}`}
10549
+ className={`transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu${chromePopping === 'zoom-out' ? ' anet-chrome-pop' : ''}`}
9929
10550
  data-topo-chrome-zoom-out-icon
9930
10551
  ><path d="M5 12h14" /></svg>
9931
10552
  </button>
@@ -10030,11 +10651,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10030
10651
  {/* R352 sibling — zoom-in icon picks up the same
10031
10652
  group-hover:scale-110 family. Mirror change at
10032
10653
  the zoom-out icon above. */}
10654
+ {/* R454 sibling — zoom-in icon picks up the same
10655
+ group-hover:[stroke-width:2.8] family lift. */}
10033
10656
  <svg
10034
10657
  width="12" height="12" viewBox="0 0 24 24"
10035
10658
  fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
10036
10659
  aria-hidden
10037
- className={`transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu${chromePopping === 'zoom-in' ? ' anet-chrome-pop' : ''}`}
10660
+ className={`transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu${chromePopping === 'zoom-in' ? ' anet-chrome-pop' : ''}`}
10038
10661
  data-topo-chrome-zoom-in-icon
10039
10662
  ><path d="M12 5v14M5 12h14" /></svg>
10040
10663
  </button>
@@ -10091,13 +10714,28 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10091
10714
  (2.5) all share one weight. View-box (24×24) and
10092
10715
  display size (13×13) unchanged, so geometry stays
10093
10716
  pixel-stable — only the stroke deepens. */}
10717
+ {/* Round 453 / Loop: chrome reset icon strokeWidth hover
10718
+ lift — 2.5 → 2.8 on hoveredReset && !resetSpinning.
10719
+ Sibling to R443 runtime badge inner-icon sw lift
10720
+ (2.4→2.8) — both chrome icons now thicken on hover
10721
+ for tactile feedback. Pre-R453 reset hover was a
10722
+ rotate-only cue (R350); R453 adds a stroke-weight
10723
+ axis so the affordance reads with both motion (R350
10724
+ rotate -8°) AND geometry (R453 sw +0.3). Gated on
10725
+ !resetSpinning so the R184 spin keyframe owns paint
10726
+ during its 450ms run. 200ms stroke-width transition
10727
+ appended to the style list matches R350 transform
10728
+ cadence. data-topo-chrome-reset-icon-stroke-width
10729
+ attr exposes the resolved value for tests. */}
10094
10730
  <svg
10095
10731
  width="13" height="13" viewBox="0 0 24 24"
10096
- fill="none" stroke="currentColor" strokeWidth="2.5"
10732
+ fill="none" stroke="currentColor"
10733
+ strokeWidth={hoveredReset && !resetSpinning ? '2.8' : '2.5'}
10097
10734
  strokeLinecap="round" strokeLinejoin="round"
10098
10735
  aria-hidden
10099
10736
  className={resetSpinning ? 'anet-reset-spin' : undefined}
10100
10737
  data-topo-chrome-reset-icon
10738
+ data-topo-chrome-reset-icon-stroke-width={hoveredReset && !resetSpinning ? '2.8' : '2.5'}
10101
10739
  // R350: hover-rotate preview of the R184 click-spin.
10102
10740
  // Gated on !resetSpinning so the anet-reset-spin keyframe
10103
10741
  // owns transform during its 450ms run. transformOrigin
@@ -10106,7 +10744,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10106
10744
  style={{
10107
10745
  transform: hoveredReset && !resetSpinning ? 'rotate(-8deg)' : 'rotate(0deg)',
10108
10746
  transformOrigin: 'center',
10109
- transition: 'transform 200ms ease-out',
10747
+ transition: 'transform 200ms ease-out, stroke-width 200ms ease-out',
10110
10748
  }}
10111
10749
  data-topo-chrome-reset-icon-hover={hoveredReset && !resetSpinning ? 'true' : 'false'}
10112
10750
  >
@@ -10176,12 +10814,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10176
10814
  gpu hint promotes the svg to its own compositor layer
10177
10815
  for crisper edges during the scale tween. Closes the
10178
10816
  chrome-strip per-icon hover-affordance arc. */}
10817
+ {/* R455 — fullscreen ENTER + EXIT icons pick up the same
10818
+ group-hover:[stroke-width:2.8] family lift as the
10819
+ zoom +/− icons (R454) and chrome reset icon (R453).
10820
+ Chrome icon hover sw lift family now 6 anchors —
10821
+ R208/R443 runtime badge + R453/R454-zoom-out/zoom-in
10822
+ + R455 fullscreen (this round). transition-[transform,
10823
+ stroke-width] expands existing transition-transform
10824
+ so the sw lift eases under R352 scale-110 cadence. */}
10179
10825
  {isFullscreen ? (
10180
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu" data-topo-chrome-fullscreen-icon="exit">
10826
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu" data-topo-chrome-fullscreen-icon="exit">
10181
10827
  <path d="M8 3v4a1 1 0 0 1-1 1H3M21 8h-4a1 1 0 0 1-1-1V3M3 16h4a1 1 0 0 1 1 1v4M16 21v-4a1 1 0 0 1 1-1h4" />
10182
10828
  </svg>
10183
10829
  ) : (
10184
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu" data-topo-chrome-fullscreen-icon="enter">
10830
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu" data-topo-chrome-fullscreen-icon="enter">
10185
10831
  <path d="M3 8V5a2 2 0 0 1 2-2h3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M21 16v3a2 2 0 0 1-2 2h-3" />
10186
10832
  </svg>
10187
10833
  )}