diff --git a/web/src/App.vue b/web/src/App.vue
index 1c67fa4..6b8feaf 100644
--- a/web/src/App.vue
+++ b/web/src/App.vue
@@ -7,6 +7,7 @@ import FocusModal from '@/components/FocusModal.vue'
import FooterDisclaimer from '@/components/FooterDisclaimer.vue'
import HeroHeadline from '@/components/HeroHeadline.vue'
import LegendRow from '@/components/LegendRow.vue'
+import NodeTooltip from '@/components/NodeTooltip.vue'
import PanelGrid from '@/components/PanelGrid.vue'
import PlayerBar from '@/components/PlayerBar.vue'
import ReachChart from '@/components/ReachChart.vue'
@@ -47,6 +48,7 @@ onMounted(async () => {
+
{{ store.state.announcement }}
diff --git a/web/src/components/NetworkView.vue b/web/src/components/NetworkView.vue
index 374912a..92be418 100644
--- a/web/src/components/NetworkView.vue
+++ b/web/src/components/NetworkView.vue
@@ -1,6 +1,7 @@
+
+
+
+
+ Student {{ hoveredNode }}
+ {{ stateText }}
+ {{ friendsText }}
+
+
+
+
+
diff --git a/web/src/composables/useNodeHover.ts b/web/src/composables/useNodeHover.ts
new file mode 100644
index 0000000..7c4bdb0
--- /dev/null
+++ b/web/src/composables/useNodeHover.ts
@@ -0,0 +1,37 @@
+import { reactive } from 'vue'
+import { useSimStore } from './useSimStore'
+
+// Node hover state (spec 5.5). The hovered student index lives in the sim
+// store so every panel can draw the cross-panel echo ring; the tooltip
+// additionally needs which panel is being pointed at and where.
+
+interface NodeHoverPosition {
+ panelId: string | null
+ clientX: number
+ clientY: number
+}
+
+const hoverPosition = reactive({ panelId: null, clientX: 0, clientY: 0 })
+
+export function useNodeHover() {
+ const store = useSimStore()
+
+ function setHoveredNode(panelId: string, nodeIndex: number, event: MouseEvent) {
+ store.state.hoveredNode = nodeIndex
+ hoverPosition.panelId = panelId
+ hoverPosition.clientX = event.clientX
+ hoverPosition.clientY = event.clientY
+ }
+
+ function moveHoveredNode(event: MouseEvent) {
+ hoverPosition.clientX = event.clientX
+ hoverPosition.clientY = event.clientY
+ }
+
+ function clearHoveredNode() {
+ store.state.hoveredNode = null
+ hoverPosition.panelId = null
+ }
+
+ return { hoverPosition, setHoveredNode, moveHoveredNode, clearHoveredNode }
+}