From b245051285e56e796d69c6af0456fa58ede45fe2 Mon Sep 17 00:00:00 2001 From: dzarlax Date: Fri, 26 Jun 2026 22:07:34 +0200 Subject: [PATCH 1/2] Make UI tooltips opaque and consistent --- .../2026-06-26-ui-tooltip-system.html | 254 ++++++++++++++++++ .../admin_readiness_redesign_contract_test.go | 6 +- internal/ui/style.go | 36 ++- internal/ui/templates/nav.html | 12 +- internal/ui/templates/pages/dashboard.html | 9 +- .../partials/admin_readiness_contract.html | 8 +- .../partials/admin_readiness_monitoring.html | 2 +- 7 files changed, 305 insertions(+), 22 deletions(-) create mode 100644 docs/ai-plans/2026-06-26-ui-tooltip-system.html diff --git a/docs/ai-plans/2026-06-26-ui-tooltip-system.html b/docs/ai-plans/2026-06-26-ui-tooltip-system.html new file mode 100644 index 0000000..2c669b1 --- /dev/null +++ b/docs/ai-plans/2026-06-26-ui-tooltip-system.html @@ -0,0 +1,254 @@ + + + + + + UI Tooltip System Plan + + + +
+
+

UI Tooltip System Plan

+

Date: 2026-06-26 · Scope: Health dashboard UI · Issue context: #157 follow-up

+
+ +
+

Task Summary

+

+ Make project tooltips readable by default and replace remaining browser-native + title tooltips with one consistent DOM/CSS tooltip pattern. +

+
+ +
+

Current Behavior

+
    +
  • The new readiness tooltip pattern uses .ui-tooltip with data-tooltip, but the surface is visually too transparent or too close to the page background, so text is hard to read.
  • +
  • Some dashboard elements still use browser-native title only, including EnergyBank helper/marker UI.
  • +
  • Navigation icon actions and admin readiness tables still rely on title for tooltip text.
  • +
  • Chart.js has its own canvas tooltip plugin configuration in internal/ui/static/charts.js; that is a separate mechanism and not the source of the transparent DOM tooltip problem.
  • +
+
+ +
+

Desired Behavior

+
    +
  • All DOM tooltips use a shared .ui-tooltip pattern with an opaque, high-contrast background in light and dark themes.
  • +
  • Tooltip text appears on hover and keyboard focus, with aria-label preserving accessible names.
  • +
  • Converted elements use data-tooltip for the visible tooltip text. Native title should be removed from converted controls to avoid duplicate browser bubbles.
  • +
  • The old browser tooltip must be disabled wherever the new tooltip is used; there should never be two competing tooltip layers on the same element.
  • +
  • Tooltips should not clip at common dashboard edges, including the right side of hero cards and table cells.
  • +
  • Chart.js canvas tooltips remain unchanged in this PR unless we find a direct visual regression there.
  • +
+
+ +
+

Assumptions

+
    +
  • "Replace all Tooltips" means all HTML/browser title tooltips in the Go templates, not Chart.js internal canvas tooltips.
  • +
  • It is acceptable to add keyboard focusability via tabindex="0" to non-interactive elements that expose explanatory tooltip text.
  • +
  • Admin-only readiness pages should be included because they currently contain title-only tooltip content.
  • +
+
+ +
+

Likely File Changes

+
    +
  • internal/ui/style.go — harden shared tooltip CSS: opaque surface, readable contrast, shadow, border, max-width, alignment helpers, focus behavior.
  • +
  • internal/ui/templates/pages/dashboard.html — convert remaining dashboard title helpers/markers to data-tooltip.
  • +
  • internal/ui/templates/nav.html — convert navigation icon tooltips to the shared pattern.
  • +
  • internal/ui/templates/partials/admin_readiness_contract.html — convert admin readiness contract cell titles.
  • +
  • internal/ui/templates/partials/admin_readiness_monitoring.html — convert monitoring reason titles.
  • +
  • internal/ui/dashboard_illness_template_test.go and internal/ui/admin_readiness_redesign_contract_test.go — update expectations from title to data-tooltip/aria-label where needed.
  • +
+
+ +
+

Implementation Steps

+
    +
  1. Inventory template-level title= usage again and classify each as DOM tooltip, accessibility label, or non-tooltip metadata.
  2. +
  3. Update .ui-tooltip CSS so the tooltip bubble uses an explicit opaque background and readable foreground instead of relying only on potentially translucent surface tokens.
  4. +
  5. Add small alignment modifiers if needed, for example right-aligned tooltips near card edges or table cells.
  6. +
  7. Convert title-only tooltip elements to class="ui-tooltip", data-tooltip, and aria-label; add tabindex="0" for focusable explanatory spans/cells.
  8. +
  9. Remove converted native title attributes to prevent duplicate tooltip UI.
  10. +
  11. Search the touched templates after conversion and verify that no converted .ui-tooltip element still carries title.
  12. +
  13. Update tests to assert the new tooltip contract.
  14. +
  15. Update this plan with actual changes, checks run, limitations, and follow-up notes after implementation.
  16. +
+
+ +
+

Test Plan

+
    +
  • Run git diff --check.
  • +
  • Run go test ./internal/ui -count=1.
  • +
  • Run go test ./... -count=1 if the localized UI test pass is clean.
  • +
  • Manual QA in browser: readiness confidence badge, readiness methodology badge, EnergyBank helper/marker, nav settings/admin/logout controls, admin readiness contract table, admin readiness monitoring table.
  • +
  • Manual QA light and dark modes where possible, including long Russian/Serbian tooltip text and narrow viewport behavior.
  • +
+
+ +
+

Manual QA Checklist

+
    +
  • Tooltip background is opaque enough that text is readable over dashboard cards and charts.
  • +
  • Hover and keyboard focus both show tooltip content.
  • +
  • No browser-native duplicate tooltip appears on converted elements.
  • +
  • Converted elements expose exactly one visible tooltip layer: the shared .ui-tooltip bubble.
  • +
  • Tooltip does not resize or shift the underlying layout.
  • +
  • Tooltip is not clipped by hero/dashboard containers or table wrappers in the common desktop viewport.
  • +
+
+ +
+

Risks

+
    +
  • Table-cell tooltips may need careful positioning so they do not hide neighboring values or create awkward keyboard tab stops.
  • +
  • Removing title changes browser-native fallback behavior; accessibility is preserved through aria-label, but visual behavior now depends on CSS.
  • +
  • Fixing clipping with overflow: visible or z-index changes can accidentally let tooltips overlap nearby controls. This needs manual QA.
  • +
  • Touch screens do not have hover. Focus/tap behavior improves this, but it will not be identical to desktop hover.
  • +
+
+ +
+

Rollback Plan

+
    +
  • Revert the tooltip CSS and template conversion commit.
  • +
  • Because this is UI-only template/CSS work, rollback does not require database or deployment-state migration.
  • +
  • If only one converted area misbehaves, restore that area to the previous title behavior while keeping the opaque tooltip CSS fix.
  • +
+
+ +
+

Open Questions

+
    +
  • Should admin table-cell tooltips be focusable with tabindex="0", or should we only show them on mouse hover to keep keyboard navigation shorter? Recommendation: make them focusable because the content is explanatory.
  • +
  • Should Chart.js canvas tooltips be redesigned in a separate pass? Recommendation: no for this PR; leave canvas chart behavior untouched unless QA reveals the same readability problem there.
  • +
+
+ +
+

Approval Gate

+

+ Production code should not be changed until this plan is approved. + After approval, implementation will proceed on a new codex/ + branch and be delivered through a pull request. +

+
+ +
+

Implementation Update

+

Status: implemented on codex/ui-tooltip-system.

+
    +
  • Added dedicated opaque tooltip tokens in internal/ui/style.go for light and dark mode.
  • +
  • Updated .ui-tooltip to use a high-contrast opaque surface, higher z-index, hidden visibility while inactive, and alignment helpers.
  • +
  • Removed native title from converted dashboard readiness tooltips so only the shared tooltip appears.
  • +
  • Converted EnergyBank helper/marker, navigation actions, and admin readiness table cells from title to data-tooltip + aria-label.
  • +
  • Updated the admin readiness contract test to assert the new data-tooltip contract.
  • +
  • Left Chart.js canvas tooltips unchanged because they are a separate plugin mechanism.
  • +
+
+ +
+

Checks Run

+
    +
  • rg -n "title=|data-tooltip|ui-tooltip" internal/ui/templates internal/ui/style.go internal/ui/*_test.go — no template title= attributes remain.
  • +
  • gofmt -w internal/ui/admin_readiness_redesign_contract_test.go
  • +
  • git diff --check
  • +
  • go test ./internal/ui -count=1
  • +
  • go test ./... -count=1
  • +
+
+ +
+

Known Limitations

+
    +
  • Manual visual QA in an authenticated browser is still recommended before or immediately after deployment, especially for admin table cells and narrow viewports.
  • +
  • Touch behavior remains inherently different from desktop hover; converted explanatory elements are keyboard-focusable where they are not already interactive.
  • +
+
+
+ + diff --git a/internal/ui/admin_readiness_redesign_contract_test.go b/internal/ui/admin_readiness_redesign_contract_test.go index cf06778..2018664 100644 --- a/internal/ui/admin_readiness_redesign_contract_test.go +++ b/internal/ui/admin_readiness_redesign_contract_test.go @@ -256,11 +256,11 @@ func TestFragmentAdminReadinessContract_RendersValueAndUnknown(t *testing.T) { if !strings.Contains(body, "unknown") { t.Errorf("fragment missing 'unknown' marker: %s", body) } - // Baseline reason now lives in the cell's title= attribute as + // Baseline reason now lives in the shared data-tooltip attribute as // part of "baseline=… · target=… · epoch=…" tooltip — confirm // it's the value-side label, not just the cell text. - if !strings.Contains(body, `title="baseline=`+storage.BaselineReasonWarmup) { - t.Errorf("fragment missing baseline_warmup title attribute: %s", body) + if !strings.Contains(body, `data-tooltip="baseline=`+storage.BaselineReasonWarmup) { + t.Errorf("fragment missing baseline_warmup tooltip attribute: %s", body) } // Pivot includes tenant column with the schema name. if !strings.Contains(body, schema) { diff --git a/internal/ui/style.go b/internal/ui/style.go index 600d71b..336acfd 100644 --- a/internal/ui/style.go +++ b/internal/ui/style.go @@ -13,6 +13,16 @@ const cssStyle = ` --card-bg: var(--surface); --card-border: var(--border); --fg: var(--text); + --tooltip-bg: #111827; + --tooltip-fg: #f9fafb; + --tooltip-border: rgba(255,255,255,0.16); + --tooltip-shadow: 0 14px 38px rgba(15,23,42,0.28); +} +[dark-mode] { + --tooltip-bg: #f8fafc; + --tooltip-fg: #111827; + --tooltip-border: rgba(15,23,42,0.18); + --tooltip-shadow: 0 18px 42px rgba(0,0,0,0.48); } body { min-height: 100vh; font-size: 15px; } @@ -173,17 +183,17 @@ body { min-height: 100vh; font-size: 15px; } .ui-tooltip::after { content: attr(data-tooltip); position: absolute; - z-index: 30; + z-index: 1000; left: 0; top: calc(100% + 8px); width: max-content; max-width: min(340px, calc(100vw - 32px)); padding: 9px 11px; - border: 1px solid var(--border); + border: 1px solid var(--tooltip-border); border-radius: 8px; - background: var(--surface); - color: var(--text); - box-shadow: var(--shadow); + background: var(--tooltip-bg); + color: var(--tooltip-fg); + box-shadow: var(--tooltip-shadow); font-size: 12px; font-weight: 500; line-height: 1.45; @@ -192,17 +202,31 @@ body { min-height: 100vh; font-size: 15px; } white-space: normal; text-align: left; opacity: 0; + visibility: hidden; pointer-events: none; transform: translateY(-2px); - transition: opacity 0.12s ease, transform 0.12s ease; + transition: opacity 0.12s ease, transform 0.12s ease, visibility 0.12s ease; } +.ui-tooltip--right::after, .readiness-trust-badge.ui-tooltip::after { left: auto; right: 0; } +.ui-tooltip--center::after { + left: 50%; + transform: translate(-50%, -2px); +} +.ui-tooltip--center:hover::after, +.ui-tooltip--center:focus-visible::after { + transform: translate(-50%, 0); +} +.ui-tooltip--cell { + cursor: help; +} .ui-tooltip:hover::after, .ui-tooltip:focus-visible::after { opacity: 1; + visibility: visible; transform: translateY(0); } diff --git a/internal/ui/templates/nav.html b/internal/ui/templates/nav.html index 641faee..e10f6ed 100644 --- a/internal/ui/templates/nav.html +++ b/internal/ui/templates/nav.html @@ -14,12 +14,16 @@ {{T .Lang "all_metrics"}} - + {{T .Lang "nav_settings"}} {{if .IsAdmin}} - + {{T .Lang "admin_title"}} @@ -28,7 +32,9 @@ {{.Label}} {{end}}
-
diff --git a/internal/ui/templates/pages/dashboard.html b/internal/ui/templates/pages/dashboard.html index b2b7f02..1983484 100644 --- a/internal/ui/templates/pages/dashboard.html +++ b/internal/ui/templates/pages/dashboard.html @@ -11,13 +11,11 @@
{{T .Lang "readiness_today_label"}} {{T .Lang "methodology_status_heuristic_personalized"}} {{if .ReadinessServing.Has}} {{.ReadinessServing.Label}} @@ -91,8 +89,9 @@
{{T .Lang "energy_label"}} - {{T .Lang "methodology_status_heuristic_prescriptive"}} {{.EnergyBank.Current}}/{{.EnergyBank.Capacity}} @@ -103,7 +102,7 @@
- {{if lt .EnergyBank.Capacity 100}}
{{end}} + {{if lt .EnergyBank.Capacity 100}}
{{end}}
{{if .EnergyBank.Components}} diff --git a/internal/ui/templates/partials/admin_readiness_contract.html b/internal/ui/templates/partials/admin_readiness_contract.html index f4609e9..046de03 100644 --- a/internal/ui/templates/partials/admin_readiness_contract.html +++ b/internal/ui/templates/partials/admin_readiness_contract.html @@ -23,10 +23,10 @@ {{.Tenant}} {{.Date}} - {{.Recovery.Text}} - {{.Passive.Text}} - {{.Chronic.Text}} - {{.Acute.Text}} + {{.Recovery.Text}} + {{.Passive.Text}} + {{.Chronic.Text}} + {{.Acute.Text}} {{end}} diff --git a/internal/ui/templates/partials/admin_readiness_monitoring.html b/internal/ui/templates/partials/admin_readiness_monitoring.html index 5c17058..3c51ba0 100644 --- a/internal/ui/templates/partials/admin_readiness_monitoring.html +++ b/internal/ui/templates/partials/admin_readiness_monitoring.html @@ -34,7 +34,7 @@ {{T $.Lang "admin_monitoring_signal_coverage"}} {{.SubScore}}/{{.TargetKind}} {{.Status}} - + {{.Coverage}}{{if .TopReason}} · {{.TopReason}}{{end}}
{{T $.Lang "admin_monitoring_window"}} {{.Window}}
{{if .Freshness}}
{{.Freshness}}
{{end}} From 43779bef363060b69eff541549b225d8d1521764 Mon Sep 17 00:00:00 2001 From: dzarlax Date: Fri, 26 Jun 2026 22:13:34 +0200 Subject: [PATCH 2/2] Preserve centered tooltip transform on hover --- internal/ui/style.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/ui/style.go b/internal/ui/style.go index 336acfd..a388d77 100644 --- a/internal/ui/style.go +++ b/internal/ui/style.go @@ -216,10 +216,6 @@ body { min-height: 100vh; font-size: 15px; } left: 50%; transform: translate(-50%, -2px); } -.ui-tooltip--center:hover::after, -.ui-tooltip--center:focus-visible::after { - transform: translate(-50%, 0); -} .ui-tooltip--cell { cursor: help; } @@ -229,6 +225,10 @@ body { min-height: 100vh; font-size: 15px; } visibility: visible; transform: translateY(0); } +.ui-tooltip--center:hover::after, +.ui-tooltip--center:focus-visible::after { + transform: translate(-50%, 0); +} /* Webhook status badge on the settings page. Live-updates every 5s while state=pending; static otherwise. Colour-coded by state so the