* fix(charts): restyle hover tooltips with soft shadow + larger radius
Match the elevation pattern of DS::Select dropdown
(shadow-lg + shadow-border-xs) and increase radius (rounded-lg →
rounded-xl) + padding (p-2 → p-3) for better breathing. Drops the
hard border in favour of the soft drop shadow.
time_series_chart_controller:
- p-2 → p-3, rounded-lg → rounded-xl
- remove border border-secondary
- add shadow-lg shadow-border-xs (theme-aware drop + border-edge)
- add explicit text-primary (was missing per #2011's drift note)
- add z-50 (matches goal_projection + sankey controllers)
sankey_chart_controller:
- bg-gray-700 text-white → bg-container text-primary (theme-aware;
was broken in light mode after dark-bg flip)
- p-2 rounded → p-3 rounded-xl
- add shadow-lg shadow-border-xs
- add font-sans for consistency with the other chart tooltips
- add privacy-sensitive class (was missing — sankey money values
were rendered in the clear with privacy mode on)
DS::Tooltip (icon-trigger help, bg-inverse) is intentionally a
different primitive and is not touched.
Refs #2011 — className consolidation into a shared module is tracked
separately and intentionally not closed by this PR; the new string
applies to two of the three call sites today (time_series, sankey).
The third (goal_projection_chart_controller) lives on the
feat/goals-v2-architecture branch and will adopt the same string when
goals v2 merges.
* fix(charts): align every chart tooltip on the borderless soft-shadow card
One visual contract for all three D3 tooltip surfaces, matching the
design reference: p-4, rounded-2xl, shadow-xl, no edge ring in light
mode. Dark mode keeps a 1px alpha-white ring since a shadow alone
disappears against dark surfaces.
- goal_projection_chart_controller drops its hand-copied class string
(it still carried the old bordered recipe — the drift this util
exists to prevent) and builds its two lines through the shared
factory: secondary date line, tabular value line.
- New content conventions exported alongside the container contract:
context line = text-xs text-secondary, values = font-medium
tabular-nums. Time-series and sankey adopt them.
- Sankey node titles now escape before .html(); user-named categories
were previously interpolated raw into the tooltip markup.
* 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.
* fix(charts): tighten tooltip padding to 10x12
* feat(charts): give sankey and goal tooltips their missing context rows
Chart tooltips answer three stacked questions: context (what am I
looking at), value (how much), relation (vs what). Time-series already
had all three; the other two were missing rows.
- Sankey links showed a bare "$X (Y%)" with no indication of which
flow was hovered. Links now lead with a swatch-dotted
"Source → Target" context line; nodes get the same dot + name
treatment, tying the card to the ribbon color. The context builder
escapes node names centrally (they're user-named categories).
- Goal projection adds a tertiary relation line — "52% of $20K
target" — computed from the payload the chart already carries, for
both the saved and projected segments. Hidden when the goal has no
positive target. Template is i18n-wired like the existing tooltip
strings (goals.show.projection.tooltip_target_relation).
Verified with Playwright against the running app: all three surfaces
pass computed-style and content assertions.
* fix(charts): drop the color dot from sankey tooltip context
The hover highlight on the diagram already identifies the ribbon; the
swatch repeated it inside the card. Names alone keep the context line
quieter.
* fix(charts): use the app's sans money treatment in tooltips, not mono
font-mono in this codebase marks code, keys, and admin surfaces; money
is sans + tabular-nums everywhere else (cards, KPIs, tables). Keep the
tabular figures for scrub stability, drop the mono.
* fix(charts): address review — glide opt-in, no shadowed var, truncate cap
- The 80ms left/top transition moved out of .chart-tooltip: it eased the
snap-positioned goal tooltip but made cursor-following tooltips
(sankey, time-series) trail the pointer by a frame. Goal projection
opts back in via inline style; the component comment documents the
split.
- setRelation reuses _draw()'s targetAmount const instead of declaring
a local 'target' that shadowed the target-date const.
- Sankey context line gets max-w-64 so truncate has a constraint to
fire against on deep flows.
- Component comment now says 10x12 padding, matching the declaration.
* Revert `schema.rb` changes
---------
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
Closes#2011.
time-series and sankey each created their cursor-following tooltip with a
duplicated className literal that had already drifted apart: time-series was
missing `text-primary` and `z-50`. Move the visual contract into
app/javascript/utils/chart_tooltip.js as CHART_TOOLTIP_CLASSES and have both
controllers reference it. Each keeps its own behavioural classes (time-series
its initial `opacity-0`; both `top-0`; sankey toggles opacity via inline
style). `privacy-sensitive` stays bundled so future copies can't drop it.
Also exports a createChartTooltip factory for the raw-DOM idiom.
goal_projection_chart_controller is not in main yet (it lands with the goals
work in #1798); it migrates to the same symbol there.
* feat(dashboard): zoom into cashflow sankey categories
Click a category node on the dashboard cashflow Sankey to focus on it and
its descendants only; a back button restores the full view. Clicking the
Cash Flow node zooms to the expense (outbound) side.
- Pure utility (app/javascript/utils/sankey_zoom.js) computes the
descendant subgraph from a clicked node, with direction inferred by
reachability from the cash flow node (outbound for expense, inbound
for income).
- Stable node ids emitted from the controller so the JS can identify
nodes across re-renders.
- Stimulus controller adds chart + zoomOutButton targets, fade
transition, and only sets a pointer cursor when a node has children.
- Node:test coverage for expense, income, cash-flow, and malformed-data
cases; \"type\": \"module\" added to package.json so the .js util is
ESM-compatible under Node.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(dashboard): extract cashflow sankey chart partial
Deduplicate sankey chart markup between inline and expanded dialog views,
and reset zoom state when chart data changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(js): rename sankey_zoom util to .mjs to drop project-wide ESM flag
Removes "type": "module" from package.json to avoid implicitly switching
every .js file in the project to ESM (a future footgun for any .js config
file added by Biome, Vite, etc.). Renames the utility to .mjs so node --test
can import the ES module directly, and adds an explicit importmap pin since
pin_all_from only globs .js/.jsm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(assets): register .mjs MIME type for Propshaft
Propshaft derives Content-Type from Mime::Type.lookup_by_extension, which
returns nil for :mjs by default. Browsers refuse to execute ES modules
served with an empty Content-Type, breaking the sankey_zoom util loaded
via importmap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Adapt holdings to number inputs
* Reviews
* FIX a small provider hardcoded name
* PR l10n request
---------
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Number.parseFloat() only recognizes dots as decimal separators, causing
inputs like "256,54" (French locale) to be parsed as "256". Added a
parseLocaleFloat utility that detects whether comma or dot is the decimal
separator and normalizes accordingly before parsing.
Fixes#1138https://claude.ai/code/session_01ThfszjiCmbDDPyb4TZqk2X