mirror of
https://github.com/we-promise/sure.git
synced 2026-06-05 10:49:01 +00:00
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.
This commit is contained in:
@@ -188,10 +188,14 @@
|
||||
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.
|
||||
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);
|
||||
@@ -200,9 +204,6 @@
|
||||
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:
|
||||
|
||||
@@ -449,6 +449,10 @@ export default class extends Controller {
|
||||
// hand-copied class string that drifted from the other charts the moment
|
||||
// the contract changed.
|
||||
const tooltip = createChartTooltip(root);
|
||||
// This tooltip snaps between discrete dates (not raw cursor positions),
|
||||
// so the glide reads as easing, not lag. Cursor-following tooltips must
|
||||
// not do this — see the .chart-tooltip comment in components.css.
|
||||
tooltip.style.transition = "left 80ms ease-out, top 80ms ease-out";
|
||||
const tooltipDate = document.createElement("div");
|
||||
tooltipDate.className = CHART_TOOLTIP_CONTEXT_CLASSES;
|
||||
const tooltipValue = document.createElement("div");
|
||||
@@ -461,12 +465,13 @@ export default class extends Controller {
|
||||
tooltip.replaceChildren(tooltipDate, tooltipValue, tooltipRelation);
|
||||
|
||||
const setRelation = (amount) => {
|
||||
const target = Number(data.target_amount) || 0;
|
||||
if (target <= 0 || !data.target_amount_short_label) {
|
||||
// `targetAmount` is _draw()'s outer const (data.target_amount) — no
|
||||
// local copy, which previously shadowed the `target` date const.
|
||||
if (targetAmount <= 0 || !data.target_amount_short_label) {
|
||||
tooltipRelation.style.display = "none";
|
||||
return;
|
||||
}
|
||||
const percent = Math.round((amount / target) * 100);
|
||||
const percent = Math.round((amount / targetAmount) * 100);
|
||||
tooltipRelation.textContent = this.targetRelationTemplateValue
|
||||
.replace("{percent}", percent)
|
||||
.replace("{target}", data.target_amount_short_label);
|
||||
|
||||
@@ -550,7 +550,9 @@ export default class extends Controller {
|
||||
// what's hovered. No color swatch — the hover highlight on the diagram
|
||||
// itself already says which ribbon the card belongs to.
|
||||
#tooltipContext(label) {
|
||||
return `<div class="text-xs text-secondary mb-1 truncate">${label}</div>`;
|
||||
// max-w-64 gives truncate a constraint to fire against — an absolute
|
||||
// tooltip otherwise grows to fit and never ellipsizes deep flows.
|
||||
return `<div class="max-w-64 text-xs text-secondary mb-1 truncate">${label}</div>`;
|
||||
}
|
||||
|
||||
#showTooltip(event, value, percentage, contextHtml = null) {
|
||||
|
||||
28
db/schema.rb
generated
28
db/schema.rb
generated
@@ -42,7 +42,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_31_213000) do
|
||||
t.index ["account_id"], name: "index_account_shares_on_account_id"
|
||||
t.index ["user_id", "include_in_finances"], name: "index_account_shares_on_user_id_and_include_in_finances"
|
||||
t.index ["user_id"], name: "index_account_shares_on_user_id"
|
||||
t.check_constraint "permission::text = ANY (ARRAY['full_control'::character varying, 'read_write'::character varying, 'read_only'::character varying]::text[])", name: "chk_account_shares_permission"
|
||||
t.check_constraint "permission::text = ANY (ARRAY['full_control'::character varying::text, 'read_write'::character varying::text, 'read_only'::character varying::text])", name: "chk_account_shares_permission"
|
||||
end
|
||||
|
||||
create_table "account_statements", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
@@ -91,9 +91,9 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_31_213000) do
|
||||
t.check_constraint "match_confidence IS NULL OR match_confidence >= 0::numeric AND match_confidence <= 1::numeric", name: "chk_account_statements_match_confidence"
|
||||
t.check_constraint "parser_confidence IS NULL OR parser_confidence >= 0::numeric AND parser_confidence <= 1::numeric", name: "chk_account_statements_parser_confidence"
|
||||
t.check_constraint "period_start_on IS NULL OR period_end_on IS NULL OR period_start_on <= period_end_on", name: "chk_account_statements_period_order"
|
||||
t.check_constraint "review_status::text = ANY (ARRAY['unmatched'::character varying, 'linked'::character varying, 'rejected'::character varying]::text[])", name: "chk_account_statements_review_status"
|
||||
t.check_constraint "review_status::text = ANY (ARRAY['unmatched'::character varying::text, 'linked'::character varying::text, 'rejected'::character varying::text])", name: "chk_account_statements_review_status"
|
||||
t.check_constraint "source::text = 'manual_upload'::text", name: "chk_account_statements_source"
|
||||
t.check_constraint "upload_status::text = ANY (ARRAY['stored'::character varying, 'failed'::character varying]::text[])", name: "chk_account_statements_upload_status"
|
||||
t.check_constraint "upload_status::text = ANY (ARRAY['stored'::character varying::text, 'failed'::character varying::text])", name: "chk_account_statements_upload_status"
|
||||
end
|
||||
|
||||
create_table "accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
@@ -106,7 +106,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_31_213000) do
|
||||
t.uuid "accountable_id"
|
||||
t.decimal "balance", precision: 19, scale: 4
|
||||
t.string "currency"
|
||||
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Loan'::character varying, 'CreditCard'::character varying, 'OtherLiability'::character varying])::text[])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
|
||||
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Loan'::character varying)::text, ('CreditCard'::character varying)::text, ('OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
|
||||
t.uuid "import_id"
|
||||
t.uuid "plaid_account_id"
|
||||
t.decimal "cash_balance", precision: 19, scale: 4, default: "0.0"
|
||||
@@ -117,8 +117,8 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_31_213000) do
|
||||
t.string "institution_domain"
|
||||
t.text "notes"
|
||||
t.uuid "owner_id"
|
||||
t.datetime "disabled_at"
|
||||
t.integer "account_providers_count", default: 0, null: false
|
||||
t.datetime "disabled_at"
|
||||
t.index ["accountable_id", "accountable_type"], name: "index_accounts_on_accountable_id_and_accountable_type"
|
||||
t.index ["accountable_type"], name: "index_accounts_on_accountable_type"
|
||||
t.index ["currency"], name: "index_accounts_on_currency"
|
||||
@@ -547,7 +547,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_31_213000) do
|
||||
t.index ["provider_key"], name: "index_debug_log_entries_on_provider_key"
|
||||
t.index ["source"], name: "index_debug_log_entries_on_source"
|
||||
t.index ["user_id"], name: "index_debug_log_entries_on_user_id"
|
||||
t.check_constraint "level::text = ANY (ARRAY['debug'::character varying, 'info'::character varying, 'warn'::character varying, 'error'::character varying]::text[])", name: "chk_debug_log_entries_level"
|
||||
t.check_constraint "level::text = ANY (ARRAY['debug'::character varying::text, 'info'::character varying::text, 'warn'::character varying::text, 'error'::character varying::text])", name: "chk_debug_log_entries_level"
|
||||
end
|
||||
|
||||
create_table "depositories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
@@ -767,7 +767,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_31_213000) do
|
||||
t.string "default_account_sharing", default: "shared", null: false
|
||||
t.string "enabled_currencies", array: true
|
||||
t.datetime "last_sync_all_attempted_at"
|
||||
t.check_constraint "default_account_sharing::text = ANY (ARRAY['shared'::character varying, 'private'::character varying]::text[])", name: "chk_families_default_account_sharing"
|
||||
t.check_constraint "default_account_sharing::text = ANY (ARRAY['shared'::character varying::text, 'private'::character varying::text])", name: "chk_families_default_account_sharing"
|
||||
t.check_constraint "month_start_day >= 1 AND month_start_day <= 28", name: "month_start_day_range"
|
||||
end
|
||||
|
||||
@@ -849,7 +849,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_31_213000) do
|
||||
t.index ["family_id", "state"], name: "index_goals_on_family_id_and_state"
|
||||
t.index ["family_id"], name: "index_goals_on_family_id"
|
||||
t.check_constraint "char_length(name::text) <= 255", name: "chk_savings_goals_name_length"
|
||||
t.check_constraint "state::text = ANY (ARRAY['active'::character varying, 'paused'::character varying, 'completed'::character varying, 'archived'::character varying]::text[])", name: "chk_savings_goals_state_enum"
|
||||
t.check_constraint "state::text = ANY (ARRAY['active'::character varying::text, 'paused'::character varying::text, 'completed'::character varying::text, 'archived'::character varying::text])", name: "chk_savings_goals_state_enum"
|
||||
t.check_constraint "target_amount > 0::numeric", name: "chk_savings_goals_target_amount_positive"
|
||||
end
|
||||
|
||||
@@ -999,10 +999,10 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_31_213000) do
|
||||
t.index ["id", "family_id"], name: "idx_import_sessions_on_id_family", unique: true
|
||||
t.check_constraint "client_session_id IS NULL OR btrim(client_session_id::text) <> ''::text", name: "chk_import_sessions_client_session_id_present"
|
||||
t.check_constraint "expected_chunks IS NULL OR expected_chunks > 0", name: "chk_import_sessions_expected_chunks_positive"
|
||||
t.check_constraint "jsonb_typeof(error_details) = 'object'::text", name: "chk_import_sessions_error_details_object"
|
||||
t.check_constraint "import_type::text = 'SureImport'::text", name: "chk_import_sessions_import_type"
|
||||
t.check_constraint "status::text = ANY (ARRAY['pending'::character varying, 'importing'::character varying, 'complete'::character varying, 'failed'::character varying]::text[])", name: "chk_import_sessions_status"
|
||||
t.check_constraint "jsonb_typeof(error_details) = 'object'::text", name: "chk_import_sessions_error_details_object"
|
||||
t.check_constraint "jsonb_typeof(summary) = 'object'::text", name: "chk_import_sessions_summary_object"
|
||||
t.check_constraint "status::text = ANY (ARRAY['pending'::character varying, 'importing'::character varying, 'complete'::character varying, 'failed'::character varying]::text[])", name: "chk_import_sessions_status"
|
||||
end
|
||||
|
||||
create_table "import_source_mappings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
@@ -1020,10 +1020,10 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_31_213000) do
|
||||
t.index ["import_session_id"], name: "index_import_source_mappings_on_import_session_id"
|
||||
t.index ["target_type", "target_id"], name: "idx_import_source_mappings_on_target"
|
||||
t.check_constraint "btrim(source_id::text) <> ''::text", name: "chk_import_source_mappings_source_id_present"
|
||||
t.check_constraint "source_type::text = ANY (ARRAY['Account'::character varying, 'Category'::character varying, 'Tag'::character varying, 'Merchant'::character varying, 'RecurringTransaction'::character varying, 'Transaction'::character varying, 'Budget'::character varying, 'Security'::character varying, 'Rule'::character varying]::text[])", name: "chk_import_source_mappings_source_type"
|
||||
t.check_constraint "btrim(source_type::text) <> ''::text", name: "chk_import_source_mappings_source_type_present"
|
||||
t.check_constraint "target_type::text = ANY (ARRAY['Account'::character varying, 'Category'::character varying, 'Tag'::character varying, 'Merchant'::character varying, 'RecurringTransaction'::character varying, 'Transaction'::character varying, 'Budget'::character varying, 'Security'::character varying, 'Rule'::character varying]::text[])", name: "chk_import_source_mappings_target_type"
|
||||
t.check_constraint "btrim(target_type::text) <> ''::text", name: "chk_import_source_mappings_target_type_present"
|
||||
t.check_constraint "source_type::text = ANY (ARRAY['Account'::character varying, 'Category'::character varying, 'Tag'::character varying, 'Merchant'::character varying, 'RecurringTransaction'::character varying, 'Transaction'::character varying, 'Budget'::character varying, 'Security'::character varying, 'Rule'::character varying]::text[])", name: "chk_import_source_mappings_source_type"
|
||||
t.check_constraint "target_type::text = ANY (ARRAY['Account'::character varying, 'Category'::character varying, 'Tag'::character varying, 'Merchant'::character varying, 'RecurringTransaction'::character varying, 'Transaction'::character varying, 'Budget'::character varying, 'Security'::character varying, 'Rule'::character varying]::text[])", name: "chk_import_source_mappings_target_type"
|
||||
end
|
||||
|
||||
create_table "imports", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
@@ -1078,9 +1078,9 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_31_213000) do
|
||||
t.index ["import_session_id"], name: "index_imports_on_import_session_id"
|
||||
t.check_constraint "checksum IS NULL OR length(checksum::text) = 64", name: "chk_imports_checksum_sha256_length"
|
||||
t.check_constraint "client_chunk_id IS NULL OR btrim(client_chunk_id::text) <> ''::text", name: "chk_imports_client_chunk_id_present"
|
||||
t.check_constraint "jsonb_typeof(error_details) = 'object'::text", name: "chk_imports_error_details_object"
|
||||
t.check_constraint "import_session_id IS NULL OR checksum IS NOT NULL", name: "chk_imports_session_checksum_present"
|
||||
t.check_constraint "import_session_id IS NULL OR sequence IS NOT NULL", name: "chk_imports_session_sequence_present"
|
||||
t.check_constraint "jsonb_typeof(error_details) = 'object'::text", name: "chk_imports_error_details_object"
|
||||
t.check_constraint "jsonb_typeof(summary) = 'object'::text", name: "chk_imports_summary_object"
|
||||
t.check_constraint "sequence IS NULL OR sequence > 0", name: "chk_imports_session_sequence_positive"
|
||||
end
|
||||
@@ -1616,7 +1616,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_31_213000) do
|
||||
t.index ["kind"], name: "index_securities_on_kind"
|
||||
t.index ["price_provider", "offline_reason"], name: "index_securities_on_price_provider_and_offline_reason"
|
||||
t.index ["price_provider"], name: "index_securities_on_price_provider"
|
||||
t.check_constraint "kind::text = ANY (ARRAY['standard'::character varying, 'cash'::character varying]::text[])", name: "chk_securities_kind"
|
||||
t.check_constraint "kind::text = ANY (ARRAY['standard'::character varying::text, 'cash'::character varying::text])", name: "chk_securities_kind"
|
||||
end
|
||||
|
||||
create_table "security_prices", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
|
||||
Reference in New Issue
Block a user