Files
sure/app/assets/tailwind/sure-design-system/components.css
Guillem Arias Fauste 6d27285c03 fix(charts): restyle hover tooltips with soft shadow + larger radius (#2029)
* 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>
2026-06-08 21:58:37 +02:00

251 lines
8.6 KiB
CSS

@layer components {
/* Forms */
.form-field {
@apply flex flex-col gap-1 relative px-3 py-2 rounded-md border bg-container border-secondary shadow-xs w-full;
@apply focus-within:border-secondary focus-within:shadow-none focus-within:ring-4 focus-within:ring-alpha-black-200;
@apply transition-all duration-300;
@variant theme-dark {
@apply focus-within:ring-alpha-white-300;
}
/* Add styles for multiple select within form fields */
select[multiple] {
@apply py-2 pr-2 space-y-0.5 overflow-y-auto;
option {
@apply py-2 rounded-md;
}
option:checked {
@apply after:content-['\2713'] bg-container-inset after:text-gray-500 after:ml-2;
}
option:active,
option:focus {
@apply bg-container-inset;
}
}
}
/* New form field structure components */
.form-field__header {
@apply flex items-center justify-between gap-2;
}
.form-field__body {
@apply flex flex-col gap-1;
}
.form-field__actions {
@apply flex items-center gap-1;
}
.form-field__label {
@apply block text-xs text-secondary peer-disabled:text-subdued;
}
.form-field__input {
@apply text-primary border-none bg-container text-sm opacity-100 w-full p-0;
@apply focus:opacity-100 focus:outline-hidden focus:ring-0;
@apply placeholder-shown:opacity-50;
@apply disabled:text-subdued;
@apply text-ellipsis overflow-hidden whitespace-nowrap;
@apply transition-opacity duration-300;
@apply placeholder:text-subdued;
@variant theme-dark {
&::-webkit-calendar-picker-indicator {
filter: invert(1);
cursor: pointer;
}
}
}
textarea.form-field__input {
@apply whitespace-normal overflow-auto;
text-overflow: clip;
}
.form-field__input--multiselect-trigger {
@apply whitespace-normal overflow-visible;
text-overflow: clip;
}
select.form-field__input,
button.form-field__input {
@apply pr-10 appearance-none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right -0.15rem center;
background-repeat: no-repeat;
background-size: 1.25rem 1.25rem;
text-align: left;
}
.form-field__radio {
@apply text-primary;
}
.form-field__submit {
@apply cursor-pointer rounded-lg bg-surface p-3 text-center text-white hover:bg-surface-hover;
}
/* Checkboxes */
.checkbox {
&[type='checkbox'] {
@apply rounded-sm;
@apply transition-colors duration-300;
}
}
.checkbox--light {
&[type='checkbox'] {
@apply border-alpha-black-200 checked:bg-gray-900 checked:ring-gray-900 focus:ring-gray-900 focus-visible:ring-gray-900 checked:hover:bg-gray-300 hover:bg-gray-300;
}
&[type='checkbox']:disabled {
@apply cursor-not-allowed opacity-80 bg-gray-50 border-gray-200 checked:bg-gray-400 checked:ring-gray-400;
}
@variant theme-dark {
&[type='checkbox'] {
@apply ring-gray-900 border-alpha-white-300;
background-color: transparent;
}
&[type='checkbox']:disabled {
@apply cursor-not-allowed opacity-80 border-transparent;
background-color: var(--color-gray-700);
}
&[type='checkbox']:checked,
&[type='checkbox']:indeterminate {
@apply border-transparent;
background-color: var(--color-gray-100);
}
&[type='checkbox']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='%23171717' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}
&[type='checkbox']:indeterminate {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='%23171717' xmlns='http://www.w3.org/2000/svg'%3e%3crect x='3.5' y='7' width='9' height='2' rx='1'/%3e%3c/svg%3e");
}
}
}
.checkbox--dark {
&[type='checkbox'] {
@apply ring-gray-900 checked:text-white;
}
&[type='checkbox']:disabled {
@apply cursor-not-allowed opacity-80 ring-gray-600;
}
&[type='checkbox']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='%23111827' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}
}
/* Tooltips */
.tooltip {
@apply hidden absolute;
}
.qrcode svg path {
fill: var(--color-black);
@variant theme-dark {
fill: var(--color-white);
}
}
/*
Segmented control (#2137) — track + equal-footprint segments. The selected
state is a single `--active` class so an optional Stimulus controller can
toggle it as one unit. Values mirror the DS tab tokens (tab-bg-group /
tab-item-active / tab-item-hover) but are inlined with `@variant theme-dark`
because `@apply`-ing a custom utility drops its dark override. The near-black
dark track (alpha-black-700) is what makes the gray-700 selected pill read —
the bespoke controls used a too-light `container-inset` track, hence the
audit's "selected pill invisible on dark." Focus = neutral outline (matches
the canonical focus ring; inlined until that token lands on main).
*/
.segmented-control {
@apply inline-flex items-center gap-0.5 p-1 rounded-lg bg-gray-50;
@variant theme-dark {
@apply bg-alpha-black-700;
}
}
.segmented-control__segment {
@apply inline-flex items-center justify-center px-2 py-1 rounded-md whitespace-nowrap;
@apply text-sm font-medium text-secondary cursor-pointer transition-colors duration-200;
@apply hover:bg-gray-200;
@apply focus-visible:outline-2 focus-visible:outline-offset-2;
@apply focus-visible:outline-alpha-black-400 theme-dark:focus-visible:outline-alpha-white-400;
@variant theme-dark {
@apply hover:bg-gray-800;
}
}
.segmented-control__segment--active {
@apply bg-white text-primary shadow-sm hover:bg-white;
@variant theme-dark {
@apply bg-gray-700 hover:bg-gray-700;
}
}
/*
Horizontally scrollable table wrapper (#2137). `overflow-x: auto` so wide
tables scroll instead of clipping (the LLM-usage table was `overflow-hidden`
and pushed columns off-screen) or wrapping money mid-digit. The pure-CSS
"scroll shadow" gives the missing affordance: the cover gradients
(`--table-scroll-bg`, default container-inset) scroll WITH the content
(`background-attachment: local`) and hide the fixed shadow gradients at the
edges, so a soft edge-shadow only appears when there is more to scroll.
Theme-aware via `--color-shadow`. Set `--table-scroll-bg` to match the wrapper.
*/
.table-scroll {
--table-scroll-bg: var(--color-container-inset);
overflow-x: auto;
background:
linear-gradient(to right, var(--table-scroll-bg) 30%, transparent),
linear-gradient(to left, var(--table-scroll-bg) 30%, transparent) 100% 0,
radial-gradient(farthest-side at 0 50%, var(--color-shadow), transparent),
radial-gradient(farthest-side at 100% 50%, var(--color-shadow), transparent) 100% 0;
background-repeat: no-repeat;
background-color: var(--table-scroll-bg);
background-size: 32px 100%, 32px 100%, 12px 100%, 12px 100%;
background-attachment: local, local, scroll, scroll;
}
/*
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, 10x12 padding. Dark
mode swaps the ring to alpha-white; the drop shadow is near-invisible
there, which is fine — the ring carries the edge.
No position transition here on purpose: cursor-following tooltips
(sankey, time-series) update left/top every mousemove and a transition
makes them trail the pointer. Snap-positioned tooltips (goal projection,
which jumps between dates) opt into the 80ms glide via inline style.
*/
.chart-tooltip {
background: var(--color-container);
border-radius: 10px;
padding: 10px 12px;
box-shadow:
0 0 0 1px var(--color-alpha-black-50),
0 8px 24px rgba(11, 11, 11, 0.12);
@variant theme-dark {
box-shadow:
0 0 0 1px var(--color-alpha-white-50),
0 8px 24px rgba(11, 11, 11, 0.12);
}
}
}