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.
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.
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.
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