From 53b18e30dffdcc7b1d716798ad965ded66aff65c Mon Sep 17 00:00:00 2001 From: Cyber Preacher <72062250+Cyber-preacher@users.noreply.github.com> Date: Thu, 11 Jun 2026 21:35:28 +0400 Subject: [PATCH 1/7] Render Invision system state details --- src/pages/invision/Invision.tsx | 50 +++++++++++++++++++++++++++------ src/types/api.ts | 3 ++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/pages/invision/Invision.tsx b/src/pages/invision/Invision.tsx index 349b059..cf7193f 100644 --- a/src/pages/invision/Invision.tsx +++ b/src/pages/invision/Invision.tsx @@ -47,6 +47,15 @@ function toneForRiskStatus(status: string) { return "primary"; } +function toneForSystemState( + tone?: GetInvisionResponse["governanceState"]["tone"], +) { + if (tone === "critical") return "danger"; + if (tone === "strong" || tone === "stable") return "ok"; + if (tone === "watch") return "warn"; + return "neutral"; +} + function EngineSection({ engine, title, @@ -59,7 +68,7 @@ function EngineSection({ {engine.confidence}% · {engine.confidenceBand} @@ -184,13 +193,38 @@ const Invision: React.FC = () => { ) : null} - -

- Governance model -

-

- {invision?.governanceState.label ?? "—"} -

+ +
+
+

+ Governance model +

+

+ {invision?.governanceState.label ?? "—"} +

+ {invision?.governanceState.summary ? ( +

+ {invision.governanceState.summary} +

+ ) : null} +
+ + {invision?.governanceState.tone ?? "unknown"} + +
+ {(invision?.governanceState.drivers ?? []).length > 0 ? ( + + {(invision?.governanceState.drivers ?? []).map((driver) => ( + + ))} + + ) : null}
{primaryGovernanceMetrics.map((metric) => ( diff --git a/src/types/api.ts b/src/types/api.ts index 5811815..529d38c 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -362,6 +362,9 @@ export type GetFormationResponse = { export type InvisionGovernanceMetricDto = { label: string; value: string }; export type InvisionGovernanceStateDto = { label: string; + tone?: "critical" | "watch" | "stable" | "strong" | "unknown"; + summary?: string; + drivers?: string[]; metrics: InvisionGovernanceMetricDto[]; }; export type InvisionStabilityComponentDto = { From 4f931dbabf895c437e47228ce9aa012507ea66e0 Mon Sep 17 00:00:00 2001 From: Cyber Preacher <72062250+Cyber-preacher@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:06:16 +0400 Subject: [PATCH 2/7] Clarify Invision score and evidence coverage --- src/pages/invision/Invision.tsx | 43 +++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src/pages/invision/Invision.tsx b/src/pages/invision/Invision.tsx index cf7193f..1818bb9 100644 --- a/src/pages/invision/Invision.tsx +++ b/src/pages/invision/Invision.tsx @@ -32,14 +32,18 @@ function toneForScore(tone: InvisionStabilityComponentDto["tone"]) { return "ok"; } -function toneForConfidence(engine: EngineDto): StatusTone { - if (engine.confidenceBand === "High" || engine.confidence >= 75) return "ok"; - if (engine.confidenceBand === "Medium" || engine.confidence >= 50) { - return "warn"; - } +function toneForHealthScore(score: number): StatusTone { + if (score >= 67) return "ok"; + if (score >= 34) return "warn"; return "danger"; } +function toneForMetricValue(value: string): StatusTone { + const percentValue = Number(value.replace("%", "").trim()); + if (!Number.isFinite(percentValue)) return "neutral"; + return toneForHealthScore(percentValue); +} + function toneForRiskStatus(status: string) { const normalized = status.trim().toLowerCase(); if (normalized === "critical") return "danger"; @@ -65,12 +69,27 @@ function EngineSection({ }) { return ( - - + + + {engine.score}% + + } + /> + + {engine.band} + + } + /> + {engine.confidence}% · {engine.confidenceBand} } @@ -231,7 +250,11 @@ const Invision: React.FC = () => { + {metric.value} + + } /> ))} From 44ba0b0bdbb0a8f8875123e2775e4a163fa566c9 Mon Sep 17 00:00:00 2001 From: Cyber Preacher <72062250+Cyber-preacher@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:31:02 +0400 Subject: [PATCH 3/7] Restore Invision system state header --- src/pages/invision/Invision.tsx | 48 +++++---------------------------- 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/src/pages/invision/Invision.tsx b/src/pages/invision/Invision.tsx index 1818bb9..f96c9f3 100644 --- a/src/pages/invision/Invision.tsx +++ b/src/pages/invision/Invision.tsx @@ -51,15 +51,6 @@ function toneForRiskStatus(status: string) { return "primary"; } -function toneForSystemState( - tone?: GetInvisionResponse["governanceState"]["tone"], -) { - if (tone === "critical") return "danger"; - if (tone === "strong" || tone === "stable") return "ok"; - if (tone === "watch") return "warn"; - return "neutral"; -} - function EngineSection({ engine, title, @@ -212,38 +203,13 @@ const Invision: React.FC = () => { ) : null} - -
-
-

- Governance model -

-

- {invision?.governanceState.label ?? "—"} -

- {invision?.governanceState.summary ? ( -

- {invision.governanceState.summary} -

- ) : null} -
- - {invision?.governanceState.tone ?? "unknown"} - -
- {(invision?.governanceState.drivers ?? []).length > 0 ? ( - - {(invision?.governanceState.drivers ?? []).map((driver) => ( - - ))} - - ) : null} + +

+ Governance model +

+

+ {invision?.governanceState.label ?? "—"} +

{primaryGovernanceMetrics.map((metric) => ( From 3ad17d4de3b0f8b1e32ba87eb95e8521e120e0eb Mon Sep 17 00:00:00 2001 From: Cyber Preacher <72062250+Cyber-preacher@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:42:02 +0400 Subject: [PATCH 4/7] Explain Invision band --- src/data/vortexopedia.ts | 31 ++++++++++++++++++++++ src/pages/invision/Invision.tsx | 2 +- tests/unit/phase89-visual-contract.test.ts | 3 ++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/data/vortexopedia.ts b/src/data/vortexopedia.ts index c0f236c..7884650 100644 --- a/src/data/vortexopedia.ts +++ b/src/data/vortexopedia.ts @@ -1209,6 +1209,37 @@ export const vortexopediaTerms: VortexopediaTerm[] = [ source: "Discussion", updated: "2025-12-04", }, + { + ref: 43.1, + id: "invision_band", + name: "Invision band", + category: "governance", + short: + "A readable health label for an Invision score, grouping the raw percentage into a low, medium, or high condition.", + long: [ + "Invision engines calculate raw scores for system dimensions such as decentralization and stability.", + "The band translates that percentage into a quick status label so a user can scan the system without interpreting every component immediately.", + "The band does not replace the score. The score is the numerical result; the band is the human-readable bucket for that result.", + ], + tags: ["invision", "system_health", "score", "status", "band"], + related: [ + "gradual_decentralization", + "constant_deterrence", + "legitimacy_referendum", + ], + examples: [ + "A stability score of 28% can sit in a low band, while a score near 80% can sit in a high band.", + ], + stages: ["global"], + links: [ + { + label: "Invision", + url: "/app/invision", + }, + ], + source: "App UX: Invision system health", + updated: "2026-06-19", + }, { ref: 44, id: "voter_apathy", diff --git a/src/pages/invision/Invision.tsx b/src/pages/invision/Invision.tsx index f96c9f3..b9c307c 100644 --- a/src/pages/invision/Invision.tsx +++ b/src/pages/invision/Invision.tsx @@ -70,7 +70,7 @@ function EngineSection({ } /> Band} value={ {engine.band} diff --git a/tests/unit/phase89-visual-contract.test.ts b/tests/unit/phase89-visual-contract.test.ts index 5c1b67d..e7a1442 100644 --- a/tests/unit/phase89-visual-contract.test.ts +++ b/tests/unit/phase89-visual-contract.test.ts @@ -46,7 +46,8 @@ test("Phase 89 static visual contract covers public entry routes", () => { assert.match(guide, /The two UX primitives: hints and stages/); assert.match(vortexopedia, /Search terms/); - assert.match(vortexopedia, /Showing 56 \/ 56 entries/); + assert.match(vortexopedia, /Showing 57 \/ 57 entries/); + assert.match(vortexopedia, /Invision band/); assert.match(vortexopedia, /Vortex/); }); From fc2177b91003e7d4dbb7248b00783d85928f34fa Mon Sep 17 00:00:00 2001 From: Cyber Preacher <72062250+Cyber-preacher@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:57:29 +0400 Subject: [PATCH 5/7] Fix hover hint overlays --- src/components/Hint.tsx | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/components/Hint.tsx b/src/components/Hint.tsx index 45b1d8a..64654b1 100644 --- a/src/components/Hint.tsx +++ b/src/components/Hint.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; +import { createPortal } from "react-dom"; import { useNavigate } from "react-router"; import { Card, @@ -41,6 +42,10 @@ const useHoverOverlay = (dwellMs: number) => { hoverTimer.current = window.setTimeout(() => setStable(true), dwellMs); }; + const moveTo = (pos: OverlayPosition) => { + setPosition(pos); + }; + const hide = (force = false) => { if (!stable || force) { clearTimers(); @@ -69,11 +74,13 @@ const useHoverOverlay = (dwellMs: number) => { } }, showAt, + moveTo, hide, }; }; type OverlayPortalProps = { + interactive: boolean; visible: boolean; position: OverlayPosition; children: React.ReactNode; @@ -81,22 +88,25 @@ type OverlayPortalProps = { // Thin portal that positions content near the cursor. const OverlayPortal: React.FC = ({ + interactive, visible, position, children, }) => { - if (!visible) return null; - return ( + if (!visible || typeof document === "undefined") return null; + const overlay = (
{children}
); + return createPortal(overlay, document.body); }; type HintSurfaceProps = { @@ -173,6 +183,9 @@ export const Hint: React.FC = ({ onMouseEnter={(e) => overlay.showAt({ x: e.clientX ?? 0, y: e.clientY ?? 0 }) } + onMouseMove={(e) => + overlay.moveTo({ x: e.clientX ?? 0, y: e.clientY ?? 0 }) + } onMouseLeave={() => { overlay.setHovering(false); overlay.hide(); @@ -180,7 +193,11 @@ export const Hint: React.FC = ({ > {children} - +
overlay.setHovering(true)} onMouseLeave={() => { From 0a771df50a00b1165c2543f1453cd6af45ff1827 Mon Sep 17 00:00:00 2001 From: Cyber Preacher <72062250+Cyber-preacher@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:10:20 +0400 Subject: [PATCH 6/7] Anchor hover hints to terms --- src/components/Hint.tsx | 45 ++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/components/Hint.tsx b/src/components/Hint.tsx index 64654b1..9e55de5 100644 --- a/src/components/Hint.tsx +++ b/src/components/Hint.tsx @@ -14,6 +14,34 @@ import "./Hint.css"; type OverlayPosition = { x: number; y: number }; +const OVERLAY_WIDTH = 320; +const OVERLAY_HEIGHT = 240; +const OVERLAY_GAP = 10; +const VIEWPORT_MARGIN = 12; + +function clamp(value: number, min: number, max: number) { + return Math.min(Math.max(value, min), max); +} + +function getAnchorPosition(element: HTMLElement): OverlayPosition { + const rect = element.getBoundingClientRect(); + const maxLeft = window.innerWidth - OVERLAY_WIDTH - VIEWPORT_MARGIN; + const maxTop = window.innerHeight - OVERLAY_HEIGHT - VIEWPORT_MARGIN; + + return { + x: clamp( + rect.left + rect.width / 2 - OVERLAY_WIDTH / 2, + VIEWPORT_MARGIN, + Math.max(VIEWPORT_MARGIN, maxLeft), + ), + y: clamp( + rect.bottom + OVERLAY_GAP, + VIEWPORT_MARGIN, + Math.max(VIEWPORT_MARGIN, maxTop), + ), + }; +} + // Headless hover logic: track position, visibility, and “stable” state after dwell. const useHoverOverlay = (dwellMs: number) => { const [visible, setVisible] = useState(false); @@ -42,10 +70,6 @@ const useHoverOverlay = (dwellMs: number) => { hoverTimer.current = window.setTimeout(() => setStable(true), dwellMs); }; - const moveTo = (pos: OverlayPosition) => { - setPosition(pos); - }; - const hide = (force = false) => { if (!stable || force) { clearTimers(); @@ -74,7 +98,6 @@ const useHoverOverlay = (dwellMs: number) => { } }, showAt, - moveTo, hide, }; }; @@ -86,7 +109,8 @@ type OverlayPortalProps = { children: React.ReactNode; }; -// Thin portal that positions content near the cursor. +// Thin portal that positions content near the hovered term while escaping +// clipping and stacking contexts created by page surfaces. const OverlayPortal: React.FC = ({ interactive, visible, @@ -98,8 +122,8 @@ const OverlayPortal: React.FC = ({
@@ -181,10 +205,7 @@ export const Hint: React.FC = ({ noUnderline && "no-underline", )} onMouseEnter={(e) => - overlay.showAt({ x: e.clientX ?? 0, y: e.clientY ?? 0 }) - } - onMouseMove={(e) => - overlay.moveTo({ x: e.clientX ?? 0, y: e.clientY ?? 0 }) + overlay.showAt(getAnchorPosition(e.currentTarget)) } onMouseLeave={() => { overlay.setHovering(false); From 231aa3ee3bb0e8fc35322bd124db21c3382cd722 Mon Sep 17 00:00:00 2001 From: Cyber Preacher <72062250+Cyber-preacher@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:32:23 +0400 Subject: [PATCH 7/7] Format hint overlay hotfix --- src/components/Hint.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/Hint.tsx b/src/components/Hint.tsx index 9e55de5..63b8758 100644 --- a/src/components/Hint.tsx +++ b/src/components/Hint.tsx @@ -204,9 +204,7 @@ export const Hint: React.FC = ({ "hint-trigger tracking-normal whitespace-pre-wrap normal-case", noUnderline && "no-underline", )} - onMouseEnter={(e) => - overlay.showAt(getAnchorPosition(e.currentTarget)) - } + onMouseEnter={(e) => overlay.showAt(getAnchorPosition(e.currentTarget))} onMouseLeave={() => { overlay.setHovering(false); overlay.hide();