From 434886e7664fd2b82ca57a496694936a43b22808 Mon Sep 17 00:00:00 2001 From: Guillem Arias Date: Fri, 5 Jun 2026 09:22:23 +0200 Subject: [PATCH] fix(charts): match the tooltip surface to the design reference exactly The previous pass approximated the reference with utility guesses (rounded-2xl, p-4, shadow-xl, dark ring). The actual spec is a hairline border ring composed with a soft 0 8px 24px drop shadow, 10px radius, 12x14 padding, and an 80ms left/top glide. Tailwind shadow utilities can't compose a ring with a custom drop shadow, so the surface moves into the design system as .chart-tooltip (theme-aware: dark swaps the ring to alpha-white and lets it carry the edge). Money/numeric figures also pick up the reference's mono treatment: font-mono + tabular-nums on every value across time-series, sankey, and goal-projection, so digits don't jitter while the scrubber moves. --- .../sure-design-system/components.css | 27 +++++++++++++++++++ .../controllers/sankey_chart_controller.js | 2 +- .../time_series_chart_controller.js | 4 +-- app/javascript/utils/chart_tooltip.js | 19 ++++++------- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/app/assets/tailwind/sure-design-system/components.css b/app/assets/tailwind/sure-design-system/components.css index 7d814388f..0594759c3 100644 --- a/app/assets/tailwind/sure-design-system/components.css +++ b/app/assets/tailwind/sure-design-system/components.css @@ -150,4 +150,31 @@ fill: var(--color-white); } } + + /* + Chart hover tooltip surface (see utils/chart_tooltip.js for the JS-side + contract). Matches the design reference exactly: hairline border ring + composed with a soft 8/24 drop shadow (Tailwind shadow utilities don't + compose, hence the component class), 10px radius, 12x14 padding, and an + 80ms left/top glide so the card eases between scrub positions instead of + teleporting. Dark mode swaps the ring to alpha-white; the drop shadow is + near-invisible there, which is fine — the ring carries the edge. + */ + .chart-tooltip { + background: var(--color-container); + border-radius: 10px; + padding: 12px 14px; + box-shadow: + 0 0 0 1px var(--color-alpha-black-50), + 0 8px 24px rgba(11, 11, 11, 0.12); + transition: + left 80ms ease-out, + top 80ms ease-out; + + @variant theme-dark { + box-shadow: + 0 0 0 1px var(--color-alpha-white-50), + 0 8px 24px rgba(11, 11, 11, 0.12); + } + } } diff --git a/app/javascript/controllers/sankey_chart_controller.js b/app/javascript/controllers/sankey_chart_controller.js index 2219ef4b6..8b0d4376e 100644 --- a/app/javascript/controllers/sankey_chart_controller.js +++ b/app/javascript/controllers/sankey_chart_controller.js @@ -528,7 +528,7 @@ export default class extends Controller { (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c], ); - const valueLine = `${this.#formatCurrency(value)} (${percentage || 0}%)`; + const valueLine = `${this.#formatCurrency(value)} (${percentage || 0}%)`; const content = title ? `
${esc(title)}
${valueLine}
` : valueLine; diff --git a/app/javascript/controllers/time_series_chart_controller.js b/app/javascript/controllers/time_series_chart_controller.js index 0628d0315..696ce1af4 100644 --- a/app/javascript/controllers/time_series_chart_controller.js +++ b/app/javascript/controllers/time_series_chart_controller.js @@ -389,7 +389,7 @@ export default class extends Controller { ${datum.date_formatted}
-
+
${this._getTrendIcon(datum)}
@@ -400,7 +400,7 @@ export default class extends Controller { datum.trend.value === 0 ? `` : ` - + ${this._extractFormattedValue(datum.trend.value)} (${datum.trend.percent_formatted}) ` diff --git a/app/javascript/utils/chart_tooltip.js b/app/javascript/utils/chart_tooltip.js index 690f7802f..942a11cee 100644 --- a/app/javascript/utils/chart_tooltip.js +++ b/app/javascript/utils/chart_tooltip.js @@ -11,20 +11,21 @@ // Not to be confused with DS::Tooltip — that is the info-icon hint primitive // (bg-inverse, aria-describedby, anchored to a static trigger). This is a // data-card surface created and updated inside D3 handler code. -// Visual target: the borderless soft-shadow card from the design reference — -// generous padding, large radius, no edge ring in light mode (the shadow alone -// defines the surface). Dark mode keeps a 1px alpha ring because a shadow is -// nearly invisible against dark surfaces and the card would otherwise melt -// into the chart background. +// The surface itself lives in the design system as `.chart-tooltip` +// (sure-design-system/components.css): container bg, 10px radius, 12x14 +// padding, hairline ring composed with a soft 8/24 drop shadow, 80ms +// left/top glide. It's a component class because Tailwind shadow utilities +// can't compose a ring with a custom drop shadow. This constant adds the +// behavioural classes shared by every chart tooltip. export const CHART_TOOLTIP_CLASSES = - "bg-container text-primary text-sm font-sans absolute p-4 rounded-2xl shadow-xl theme-dark:ring-1 theme-dark:ring-alpha-white-200 pointer-events-none z-50 privacy-sensitive"; + "chart-tooltip text-primary text-sm font-sans absolute pointer-events-none z-50 privacy-sensitive"; // Content conventions (kept here so the controllers stay aligned): // - context line (date / node title): `text-xs text-secondary mb-1` -// - value figures: `font-medium tabular-nums`, secondary parentheticals in -// `text-secondary` +// - money / numeric figures: mono + tabular so digits don't jitter while +// the scrubber moves; secondary parentheticals in `text-secondary` export const CHART_TOOLTIP_CONTEXT_CLASSES = "text-xs text-secondary mb-1"; -export const CHART_TOOLTIP_VALUE_CLASSES = "font-medium tabular-nums"; +export const CHART_TOOLTIP_VALUE_CLASSES = "font-mono font-medium tabular-nums"; // Convenience factory for the raw-DOM idiom (no d3.select). Creates a hidden // tooltip div carrying the shared contract and appends it to `parent`.