mirror of
https://github.com/we-promise/sure.git
synced 2026-05-31 16:29:03 +00:00
7ad287c4db21710532fd8cf3c992b2b7f1d70179
1001 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
84ad60d541 |
fix(ai-chat): disable submit on empty input instead of surfacing 'Content missing' (#1697) (#1872)
* fix(ai-chat): disable submit on empty input instead of surfacing 'Content missing' (#1697) Empty-input clicks on the chat send button posted the form, which then failed Message's `validates :content, presence: true` and surfaced `Content missing` to the user. The right shape per ChatGPT / Claude UX is to prevent the submission entirely until the input contains non-whitespace content. Add a `submit` target on the icon button and have the existing chat Stimulus controller: - Initialise the button to `disabled` when no `message_hint` is set. - Toggle disabled on every input event (re-using the existing `autoResize` handler) based on `input.value.trim().length > 0`. - Pre-clear disabled when a sample question is injected. - Short-circuit the Enter-key submit path on empty content so keyboard users hit the same gate. Closes #1697 * fix(ai-chat): drop server-rendered disabled attr, keep JS-driven gate (#1697) Codex review (P1) + @JSONbored + @jjmata called out that rendering the submit button with `disabled: message_hint.blank?` would lock the form out for users without working JS (asset failure, exception during Stimulus init, etc.). Server-side validation already catches empty submits with a real error message — server-disabling the button on top of that turns a soft fail into a hard one. Remove the server-render `disabled:` attribute. The chat Stimulus controller still runs `#updateSubmitState()` on connect, on every input event, and after sample-question injection, and `handleInputKeyDown` still short-circuits empty Enter submits. With JS the UX is identical; without JS the form keeps its fallback path. --------- Co-authored-by: jeffrey701 <jeffrey701@users.noreply.github.com> |
||
|
|
208b3dec9d |
fix(transactions): migrate transaction filter searches to DS::SearchInput (#1998)
* fix(transactions): migrate filter sidebar searches to DS::SearchInput Replace hand-rolled search fields that used invalid focus:ring-gray-500 with DS::SearchInput (:embedded). Align date filter focus styles with the DS focus ring pattern. Refs #1715 * fix(transactions): localize filter search copy and align date focus ring Address validator feedback by replacing hardcoded filter input labels with i18n keys and updating date filter focus classes to the current design-system ring pattern. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
3bcc86f4a8 | refactor(imports): back PDF imports with statements (#1786) | ||
|
|
92549bb82a | feat(settings): add reviewed category merge flow (#1754) | ||
|
|
e13683c389 |
fix(enable_banking): migrate select_bank UI to DS primitives (#1997)
Replace hand-rolled Beta pill and secondary cancel link with DS::Pill and DS::Link on the bank picker dialog. Refs #1971 |
||
|
|
52083d5774 |
feat(reports): add Period Return card to Investment Performance (#1962)
* feat(reports): add Period Return card to Investment Performance tab Surfaces market-only return (absolute + %) for the selected period using net_market_flows from the balances table, excluding contributions and withdrawals. Appears in both the interactive report and the print view. * docs: remove TODOS.md; fold FX fallback caveat into PR description The single V2 item (Period Return's 1:1 FX fallback on missing rates) is now documented under Known Limitations in the PR description, so a tracked file in the repo root is redundant. * fix(investment_statement): align start_value denominator scope and FX handling Add status filter to match absolute_return, and move FX conversion into SQL so pre-period balances are found even when an account's currency was changed after balances were recorded. |
||
|
|
174f7e6be6 |
feat(binance): add full account sync and transaction processing (#1822)
* feat(binance): add full account sync and transaction processing - Fixed a bug that hindered Account setup - Wire up Binance accounts, sync statistics, and unlinked account tracking in the accounts dashboard. - Support setting a sync_start_date during Binance account setup. - Set Binance accounts' opening balance to zero to ensure the ledger builds cleanly from the actual trade history. - Expand the Binance importer and processor to handle Spot, Margin, Earn, P2P, and Futures trades and assets. - Implement TransactionBuilder to parse raw Binance trades, accurately calculating fees, base/quote asset amounts, and market values for proper ledger integration. - Update Binance API timeout (`recvWindow`) to 60,000ms to prevent connection drops. These changes provide comprehensive support for tracking Binance portfolios, ensuring accurate historical ledgers and proper visibility of sync statuses in the frontend dashboard. * refactor(binance): enforce strong params, double-entry safety, and native fiat currency support - Implement strong parameters in BinanceItemsController#complete_account_setup to satisfy Rails security guidelines. - Add robust date parsing with a grace fallback to prevent controller crashes on malformed sync start dates. - Wrap P2P transaction creations inside a database transaction block to guarantee ledger integrity and prevent orphan records. - Optimize P2P deduplication queries by batching checks for both transaction and funding external IDs. - Shift P2P entry persistence from forced USD tracking to native fiat values extracted directly from the Binance API payload. - Update BinanceAccount::ProcessorTest assertions and fixtures to validate native fiat and fee calculation logic. * fix(binance): process sync trades before caching transaction payload - Reorder Binance processor execution to insert trade records into the database prior to updating the `raw_transactions_payload` cache. This guarantees that if a database insertion fails, the cache won't prematurely mark the sync as successful, ensuring the data is retried on the next run. - Move `set_opening_anchor_balance(balance: 0)` out of the generic crypto exchange account builder and apply it specifically during Binance account creation. - Refactor date parsing in BinanceItemsController to explicitly catch `ArgumentError` via a block instead of using a blanket inline `rescue`. - Clean up the `setup_accounts` view template by removing hardcoded default translation strings. * fix(binance): enhance trade sync logic and error propagation - Pass `startTime` (from `sync_start_date`) to spot and futures trade endpoints on initial sync to optimize data fetching. - Include previously synced futures pairs alongside spot pairs when resolving relevant symbols to properly recover sold-out assets. - Re-raise exceptions in processor rescue blocks to prevent silent failures and ensure errors are correctly propagated to background jobs. - Decrease Binance API `recvWindow` from 60000ms to 5000ms to align with recommended default timeout values. |
||
|
|
b3fce37424 | fix: Keep with auto for category badge (#1963) | ||
|
|
12aff3cef7 |
fix(settings): add nil-safe operator to Current.family.can_manage_subscription? (#2006)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> |
||
|
|
326595ad71 | fix(accounts): show activity label instead of category for accounts supporting trades (#1993) | ||
|
|
bc3e5a824f |
feat: Add pagination in merchants page (#1965)
* feat: Add pagination in merchants page * fix: Add separate paginations for family/provider merchants * refactor: simplify conditions in view |
||
|
|
e0537a45e1 |
fix: Avoid overlay in provider section on mobile (#1990)
* fix: Avoid overlay in provider section on mobile * feat: Reduce gap between divs * fix: keep all the elements inside a dedicated container to avoid accessibility issues with the summary node |
||
|
|
8f5454ad29 |
fix(settings): preserve OpenAI form input on validation failure (#1862)
* fix(settings): preserve OpenAI form input on validation failure Fixes #1824. The OpenAI settings form auto-submits on blur, so typing the URI base before the model triggers cross-field validation. The rescue re-renders the page with values read from Setting.openai_*, which is still blank because the failed save was rejected — so the user's input disappears and they see 'OpenAI model is required' with no value to fix. Stash the submitted uri_base and model on rescue and prefer them over the saved Setting when rendering, so the user can finish typing the missing field and re-submit. * test(settings): cover openai_model preservation on validation fail (#1862) jjmata asked for symmetric coverage of the model field. Add a test where the user changes the URI base and clears the model in the same submit: the cross-field validation fails and the re-rendered model input must reflect the submitted (cleared) value rather than reverting to the saved model. Complements the existing uri_base preservation test. |
||
|
|
adea16f694 |
fix(views): clear Rule 2 + Rule 5 findings from weekly DS drift (#1951) (#1955)
* fix(views): clear Rule 2 + Rule 5 findings from weekly DS drift (#1951) Token swaps + i18n cleanup across the three files flagged in the weekly merged-commit drift scan. **`app/views/admin/users/index.html.erb`** - `bg-green-100 text-green-800` → `bg-success/10 text-success` (2 callsites — active-subscription badge + super_admin role legend) - `bg-surface-default` → `bg-surface` (`--color-surface-default` isn't defined; canonical token is `--color-surface`) - `bg-red-50/30 dark:bg-red-950/20` → `bg-destructive/5` (pending-invitation row highlight; functional token resolves correctly in both themes via `--color-destructive`) - Hand-rolled destructive button classes (`text-red-600`, `border-red-300`, `hover:bg-red-50`) → functional tokens (`text-destructive`, `border-destructive`, `hover:bg-destructive/10`) - Drop redundant `default:` args from `t(".roles.member", default: "Member")` and `t(".role_descriptions.member", default: "Basic user access…")` — the locale keys exist in `config/locales/views/admin/users/en.yml` **`app/views/imports/new.html.erb`** - `icon_bg_class: "bg-gray-tint-5"` → `"bg-surface-inset"` (`gray-tint-5` isn't a defined utility; `bg-surface-inset` carries the same muted-background intent and theme-swaps correctly) **`app/views/settings/profiles/show.html.erb`** - Drop redundant `default:` args from `t(".group_title", default: "Group")`, `t(".group_form_label", default: "Group name")`, and `t(".group_form_input_placeholder", default: "Enter group name")` — all three keys exist in `config/locales/views/settings/en.yml` **Deferred** to a separate PR (Rule 1 findings on admin/users): - `<details>` block (lines 54–180) → `DS::Disclosure(:card)` — bigger refactor with custom summary content + Stimulus controller attributes; warrants its own diff. - Destructive button shell → `DS::Button(:destructive)` — same reason; the class-token swap in this PR clears the immediate violation without changing the form-with structure or visual. Refs #1951. * fix(profiles): restore i18n default: args for group_* keys @jjmata + @codex correctly flagged: `settings.profiles.show.group_title`, `group_form_label`, and `group_form_input_placeholder` are defined in en.yml + 4 other locales (de, es, pl, pt-BR), but missing from 8 locales (ca, fr, nb, nl, ro, tr, zh-CN, zh-TW). With `config.i18n.fallbacks = true` those locales currently fall back to en values, so end-users see English copy rather than a translation-missing marker. The `default:` arg makes the fallback explicit at the call site without depending on the Rails fallback chain being configured a particular way — restores the original defensive behavior from before #1955. Admin/users role keys keep their `default:` removal — verified that `roles.member` and `role_descriptions.member` exist in all 8 admin/users locales (`grep -c "^\s*member:"` returns 2 for every locale file). |
||
|
|
5520bacbb8 |
fix(i18n): standardize product name interpolation in import mapping descriptions (#1956)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> |
||
|
|
98ca1608f4 |
fix(enable_banking): match bank list search against BIC, not just name (#1874)
* fix(enable_banking): match bank list search against BIC, not just name Bank-search filter on the Enable Banking bank-selection modal only indexed `aspsp[:name]`, so users searching by BIC code (e.g. `INGDDEFF`) got no results even when the bank was rendered in the list. Switch the per-item data attribute to a `name + BIC` haystack and read from it in the Stimulus controller, so either token matches. Refs #1814 * style(bank_search): apply Biome formatting to forEach callback (#1874 review) |
||
|
|
51271d9810 | Prevent Brand Fetch client ID autofill (#1914) | ||
|
|
3d182bc67a |
refactor(transactions): migrate transfer_match badges to DS::Pill (#1939)
Follow-up to #1917 — the responsive label-swap pair in `_transfer_match.html.erb` was deferred because DS::Pill has no caller-controlled `class:` arg yet. Wrapping each `DS::Pill` in a `<span>` with the responsive visibility classes (`hidden lg:inline` / `inline lg:hidden`) gets the same effect without expanding the component API — the parent span's `display` controls visibility, the child pill keeps its own `inline-flex` chrome when visible. Closes the last open callsite from #1917's deferred-list. Same tone (`:neutral`) and shape (`marker: false` rounded-full) as the other neutral status badges migrated in PR B. |
||
|
|
9182346c6c |
fix(admin/users): use parent space-y-6 for sibling section spacing (#1934)
The admin users page wraps four top-level sibling sections inside a single `bg-container rounded-xl shadow-border-xs p-4` card: 1. description paragraph 2. filter form 3. trials-expiring summary grid 4. families/groups list 5. role descriptions (`settings_section` collapsible → DS::Disclosure :card) The first three carried their own `mb-6`; the families list and the role descriptions section had no margin at all, so the families card sat flush against the role-descriptions card with zero gap — clearly broken next to the well-spaced upper sections. Apply spacing at the **layout** level: hoist `space-y-6` onto the outer container and drop the per-child `mb-6`. All five siblings now get a consistent 24px gap. No other admin or settings pages match this exact pattern (single outer card + multiple sibling sections without parent space-y) — the settings layout already wraps `<%= yield %>` in `space-y-4`, and other pages with outer cards (`api_keys/show`, `llm_usages/show`, etc.) either rely on that layout or carry their own internal `space-y-N`. |
||
|
|
ea51612ac7 |
refactor(views): migrate 6 residual inline alerts to DS::Alert (#1933)
* refactor(views): migrate 6 residual inline alerts to DS::Alert PR #1731 extended DS::Alert and migrated 9 inline alert blocks. Six hand-rolled alert blocks slipped through that sweep and stayed on raw palette tokens with no `theme-dark:` variants: - `app/views/settings/llm_usages/show.html.erb` — "About Cost Estimates" blue info block. Most visible offender: `bg-blue-50 border border-blue-200` + `text-blue-900 / text-blue-700 / text-blue-600` rendered as a bright white-blue island in dark mode (the bug spotted on the LLM usage page). - `app/views/accounts/confirm_unlink.html.erb` — yellow warning with bullet list. - `app/views/oidc_accounts/new_user.html.erb` — blue info heading. - `app/views/oidc_accounts/link.html.erb` — two blocks (yellow verify warning + blue create info). Also flips the file's pre-existing `text-gray-600` hint paragraph to `text-secondary` (caught by the `DeprecatedClasses` erb_lint rule on save). - `app/views/rules/confirm.html.erb` — AI cost notice. - `app/views/rules/confirm_all.html.erb` — AI cost notice. All six migrate to `DS::Alert.new(title:, variant:)` (with a block content slot for the rich/conditional bodies). DS::Alert resolves `bg-info/10`, `border-info/20`, etc. from the `@theme` semantic tokens, so dark mode now renders a subtle blue/yellow tint over the page surface instead of a hardcoded light-mode pill. Out of scope (left as-is, not alert-shaped): - `app/views/assistant_messages/_tool_calls.html.erb` — a tool-call display panel (not an alert; needs its own token sweep). - `app/views/import/rows/_form.html.erb` — inline cell-error tooltip (`bg-red-50 border border-red-200`) — also not alert-shaped; a future PR can swap it to `bg-destructive/10 border-destructive-subtle` once #1932 lands. Surfaced while scanning DS drift for the LLM usage page bug. Tracking issue: #1715 (closed but conceptually relevant) / #1911 (active drift patrol). * fix(oidc): keep alert description in <p>, retarget tests for DS::Alert title CI on #1933 caught three test failures introduced by migrating the two OIDC link alerts and the verify-redirect copy from hand-rolled `<h3>` / `<p>` markup to `DS::Alert`: 1. `OidcAccountsControllerTest#test_should_show_create_account_option_for_new_user` 2. `OidcAccountsControllerTest#test_does_not_show_create_account_button_when_JIT_link-only_mode` 3. `SessionsControllerTest#test_redirects_to_account_linking_when_no_OIDC_identity_exists` DS::Alert renders its `title:` slot as a `<p>` (semantically the alert heading lives on the container's `aria-labelledby`, not on a heading tag) and renders block / message content directly inside a `<div>`, not a `<p>`. The pre-migration markup used `<h3>` for the heading and `<p class="...text-blue-700">` for the description, so the tests above asserted those specific tags. Two fixes: - `app/views/oidc_accounts/link.html.erb` — wrap the html_safe description bodies in explicit `<p>` tags inside the DS::Alert block. Restores the `<p>` element the session-redirect test asserts on, and keeps the description as a semantic paragraph rather than a bare text node inside the alert container. - `test/controllers/oidc_accounts_controller_test.rb` — flip the two `assert_select "h3", text: "Create New Account"` calls to match the DS::Alert title `<p>`. The test was asserting an implementation detail of the pre-migration markup; switching to the new tag keeps the assertion meaningful (the heading text still has to render) without re-introducing an `<h3>` outside of DS::Alert. * fix(test): match Create New Account title with regex (sr-only "Info:" prefix) DS::Alert prepends `<span class="sr-only">Info:</span>` inside the title `<p>`, so the full text content is "Info: Create New Account", not "Create New Account". `assert_select "p", text: "Create New Account"` requires an exact text match and rejected the prefixed string. Switch to a regex match — keeps the heading-text assertion meaningful without coupling to the screen-reader prefix. |
||
|
|
f0e270f578 |
fix(design-system): restore dark-mode contrast on Toggle + destructive borders (#1932)
Two regressions from the recent token sweep, both producing low-contrast results in dark mode. ## DS::Toggle off-track PR #1843 (DS::Toggle a11y + token swaps) replaced the raw `bg-gray-100 theme-dark:bg-gray-700` off-track with `bg-surface-inset` for semantic alignment. `bg-surface-inset` resolves to gray-800 in dark mode, but the toggle typically sits inside `bg-container` (gray-900). The contrast ratio dropped from ~2.45:1 (gray-700 vs gray-900) to ~1.5:1 (gray-800 vs gray-900) — visibly worse than the pre-#1843 baseline and below WCAG 1.4.11 (3:1 for UI components). Most visible inside the transaction-edit modal SETTINGS section (`Exclude`, `One-time Expense`) where the off-state switches nearly vanished into the modal chrome. Introduce `--color-toggle-track` (light: gray-100, dark: gray-700) and swap `bg-surface-inset` → `bg-toggle-track` in DS::Toggle. Restores the pre-#1843 off-track contrast while keeping a semantic token (instead of the raw palette references the migration was trying to remove). ## border-destructive subtle borders PR #1849 (single-color tokens to @theme) flagged that `border-destructive/N` rendered the wrong shade (the `@utility border-destructive` block defined red-500 light, while `--color-destructive` in `@theme` is red-600 — `/N` resolves from @theme), and swapped a couple of callsites to solid `border-destructive`. Solid renders red-500/red-400 at full saturation in both modes, which reads as a loud error border on contexts that were meant to be subtle (left-rule on the provider-sync "view error details" pane, error-message box in SimpleFIN settings, alert-component border, provider connection error rows). Two callsites (`DS::Alert`, settings/providers/_connection_row) still carried the broken `border-destructive/20` / `/25` modifier — same off-shade footgun #1849 was meant to retire. Introduce `--color-destructive-subtle` (light: red-200, dark: red-800) and swap the four subtle-by-intent callsites to `border-destructive-subtle`: - app/components/DS/alert.rb (destructive variant) - app/views/settings/providers/_connection_row.html.erb (err status) - app/components/provider_sync_summary.html.erb (error-details left rule) - app/views/simplefin_items/edit.html.erb (error-message box) The handful of intentionally-loud `border-destructive` callsites (split-transaction over-allocation, blank-name account labels, etc.) keep the solid token. Regenerated `_generated.css` via `npm run tokens:build`. |
||
|
|
cc8e2abf18 |
fix(design-system): DS::Menu add :icon_sm variant for dense action lists (#1930)
PR #1840 bumped DS::Button icon-only `:md` size from `w-9 h-9` (36×36) to `w-11 h-11` (44×44) for WCAG 2.5.5 enhanced touch target. DS::Menu's `:icon` variant uses DS::Button at the default `:md` size, so every row-level "..." action-list trigger grew from 36×36 to 44×44. For dense lists where each row has a trigger — most visibly the transaction category dropdown (`category/dropdowns/_row.html.erb`) — the per-row height bump (+8px) compounds: a 5-category panel that used to fit in ~220px now wants ~260px, the badges look smaller relative to the row chrome, and the overall density that made the dropdown scannable regresses visibly. Add an `:icon_sm` variant that renders the trigger as DS::Button at `size: :sm` (32×32). Meets WCAG 2.5.8 AA (24×24) — appropriate for compact in-row triggers where 44×44 isn't required. Standalone toolbar / row-action `...` triggers should keep `:icon` for AAA. Migrate `category/dropdowns/_row.html.erb` to `:icon_sm` to restore the pre-#1840 dropdown density. |
||
|
|
e15349d57e |
refactor(misc): migrate misc badges to DS::Pill (#1751 PR D) (#1919)
* refactor(transactions): migrate 5 transaction badges to DS::Pill (#1751 PR B) Migrates the hand-rolled "Pending" / "Review recommended" / "Potential duplicate" / "Split" badges across the transaction views to the extended DS::Pill primitive from #1902. **Visual contract for badge mode** In #1902 the badge mode (`marker: false`) used `rounded-md` (chip shape) because the marker mode does. But every existing pill / status badge in the codebase uses `rounded-full` — see `settings/providers/_status_pill.html.erb`, `settings/providers/_maturity_badge.html.erb`, and the inline transaction badges this PR is migrating. To keep the visual contract consistent, this PR shifts `DS::Pill`'s badge mode to `rounded-full` (marker mode stays `rounded-md`, unchanged from #1829). The shape distinction now reads: markers are tags, badges are pills. **Callsites migrated** (5): - `app/views/transactions/_transaction.html.erb` — Pending, Review-recommended, Possible-duplicate, Split badges - `app/views/transactions/_header.html.erb` — Pending badge - `app/views/transactions/_split_parent_row.html.erb` — Split badge **Tone mapping** | Badge | Tone | Notes | |---|---|---| | Pending | `:neutral` | unchanged copy/icon, gains subtle DS-controlled bg | | Review recommended | `:neutral` | matches existing `bg-surface-inset` look | | Possible duplicate | `:warning` | DS semantic alias for the existing `text-warning` | | Split | `:neutral` | matches existing `bg-surface-inset` look | **Deferred to follow-up PRs** - `app/views/transactions/_transfer_match.html.erb` — uses two responsive-visibility variants (`hidden lg:inline-flex` for long copy, `inline-flex lg:hidden` for short). DS::Pill currently has no `class:` arg for caller-controlled wrapper classes; deferring until that lands. - `app/views/transactions/searches/filters/_badge.html.erb` — has a close button alongside the label (`button_to clear_filter_*`) and uses `rounded-3xl p-1.5` instead of a true pill. Closer to a removable filter chip — better fit for a separate `DS::FilterChip` primitive than for `DS::Pill`. Refs #1751. * refactor(misc): migrate misc badges to DS::Pill (#1751 PR D) Replaces five misc badge callsites with `DS::Pill` (badge mode: `marker: false`, `show_dot: false`) so the long-tail badges share the same shape, padding, and dark-mode tokens as the rest of the design system. No raw palette classes remain in the migrated files. Migrated: - app/views/shared/_badge.html.erb — converted to a thin shim that renders `DS::Pill`; preserves the block-content API and the `pulse: true` option (wraps the pill in `animate-pulse`). Maps `success`/`error`/`warning`/default → `:success`/`:error`/`:warning`/`:neutral`. - app/views/accounts/_tax_treatment_badge.html.erb — maps tax treatments to DS tones: `:tax_exempt → :green`, `:tax_deferred → :indigo` (was raw blue-500/10), `:tax_advantaged → :violet` (was raw purple-500/10), default → `:neutral`. - app/views/reports/_investment_performance.html.erb (line ~121, inline twin of the tax-treatment badge) — uses the same mapping via a new `tax_treatment_pill_tone` helper. - app/helpers/reports_helper.rb — replaces `tax_treatment_badge_classes` with `tax_treatment_pill_tone` (the old helper had no other callers). - app/views/import/qif_category_selections/show.html.erb (~line 86) — inline split badge → `tone: :warning`. - app/views/investment_activity/_badge.html.erb — fixed activity enum mapped to DS tones: Buy/Reinvestment → :indigo, Sell → :red, Dividend/Interest → :green, Contribution → :violet, Withdrawal → :amber, others → :gray. Skipped (true mismatches, not extendable without changing DS::Pill): - app/views/shared/_color_badge.html.erb — takes an arbitrary user-supplied color via `color-mix(in oklab, #{color} ...)`. DS::Pill only supports the fixed tone enum, so this would lose information. - app/views/categories/_badge.html.erb — same reason; renders `category.color` (arbitrary hex per record). - app/views/investment_activity/_quick_edit_badge.html.erb — interactive button with a Stimulus controller, click action, hover state, and dropdown anchor. DS::Pill renders a `<span>`; converting would destroy the interactive surface. Stack: based on `feat/ds-pill-transactions-1751-b` (PR #1917), which ships the `marker: false` → `rounded-full` badge shape this PR depends on. Refs #1751. |
||
|
|
2ce8e858b6 |
refactor(providers): migrate provider badges to DS::Pill (#1751 PR C) (#1918)
* refactor(transactions): migrate 5 transaction badges to DS::Pill (#1751 PR B) Migrates the hand-rolled "Pending" / "Review recommended" / "Potential duplicate" / "Split" badges across the transaction views to the extended DS::Pill primitive from #1902. **Visual contract for badge mode** In #1902 the badge mode (`marker: false`) used `rounded-md` (chip shape) because the marker mode does. But every existing pill / status badge in the codebase uses `rounded-full` — see `settings/providers/_status_pill.html.erb`, `settings/providers/_maturity_badge.html.erb`, and the inline transaction badges this PR is migrating. To keep the visual contract consistent, this PR shifts `DS::Pill`'s badge mode to `rounded-full` (marker mode stays `rounded-md`, unchanged from #1829). The shape distinction now reads: markers are tags, badges are pills. **Callsites migrated** (5): - `app/views/transactions/_transaction.html.erb` — Pending, Review-recommended, Possible-duplicate, Split badges - `app/views/transactions/_header.html.erb` — Pending badge - `app/views/transactions/_split_parent_row.html.erb` — Split badge **Tone mapping** | Badge | Tone | Notes | |---|---|---| | Pending | `:neutral` | unchanged copy/icon, gains subtle DS-controlled bg | | Review recommended | `:neutral` | matches existing `bg-surface-inset` look | | Possible duplicate | `:warning` | DS semantic alias for the existing `text-warning` | | Split | `:neutral` | matches existing `bg-surface-inset` look | **Deferred to follow-up PRs** - `app/views/transactions/_transfer_match.html.erb` — uses two responsive-visibility variants (`hidden lg:inline-flex` for long copy, `inline-flex lg:hidden` for short). DS::Pill currently has no `class:` arg for caller-controlled wrapper classes; deferring until that lands. - `app/views/transactions/searches/filters/_badge.html.erb` — has a close button alongside the label (`button_to clear_filter_*`) and uses `rounded-3xl p-1.5` instead of a true pill. Closer to a removable filter chip — better fit for a separate `DS::FilterChip` primitive than for `DS::Pill`. Refs #1751. * refactor(providers): migrate provider badges to DS::Pill (#1751 PR C) Migrates the provider-bucket pill/badge callsites to the extended DS::Pill primitive (badge mode, rounded-full) from #1917. Callsites migrated (3): - app/views/settings/providers/_status_pill.html.erb — provider connection status pill. Status → tone mapping: :ok → :success, :warn → :warning, :err → :error, else → :neutral. - app/views/settings/providers/_maturity_badge.html.erb — alpha/beta label. Tone :neutral, no dot. - app/views/sophtron_items/_sophtron_item.html.erb (line 27) — "manual sync" warning. Tone :warning, no dot. The settings/providers/_status_pill partial wraps DS::Pill rather than being deleted, since _connection_row still calls it via `render "settings/providers/status_pill", status: status` — keeping the partial preserves the seam without a wider refactor. Dead code removed: SettingsHelper#status_pill_classes (no remaining callers after the migration). Skipped: - app/views/simplefin_items/_activity_badge.html.erb — not actually a pill/badge. It renders <p> text with `text-warning` plus an inline icon below the heading; no rounded-full shape and no chip semantics. Migrating it would change the layout, not consolidate a pill pattern. Refs #1751. Stacks on #1917. |
||
|
|
20844923e6 |
refactor(transactions): migrate 5 transaction badges to DS::Pill (#1751 PR B) (#1917)
Migrates the hand-rolled "Pending" / "Review recommended" / "Potential duplicate" / "Split" badges across the transaction views to the extended DS::Pill primitive from #1902. **Visual contract for badge mode** In #1902 the badge mode (`marker: false`) used `rounded-md` (chip shape) because the marker mode does. But every existing pill / status badge in the codebase uses `rounded-full` — see `settings/providers/_status_pill.html.erb`, `settings/providers/_maturity_badge.html.erb`, and the inline transaction badges this PR is migrating. To keep the visual contract consistent, this PR shifts `DS::Pill`'s badge mode to `rounded-full` (marker mode stays `rounded-md`, unchanged from #1829). The shape distinction now reads: markers are tags, badges are pills. **Callsites migrated** (5): - `app/views/transactions/_transaction.html.erb` — Pending, Review-recommended, Possible-duplicate, Split badges - `app/views/transactions/_header.html.erb` — Pending badge - `app/views/transactions/_split_parent_row.html.erb` — Split badge **Tone mapping** | Badge | Tone | Notes | |---|---|---| | Pending | `:neutral` | unchanged copy/icon, gains subtle DS-controlled bg | | Review recommended | `:neutral` | matches existing `bg-surface-inset` look | | Possible duplicate | `:warning` | DS semantic alias for the existing `text-warning` | | Split | `:neutral` | matches existing `bg-surface-inset` look | **Deferred to follow-up PRs** - `app/views/transactions/_transfer_match.html.erb` — uses two responsive-visibility variants (`hidden lg:inline-flex` for long copy, `inline-flex lg:hidden` for short). DS::Pill currently has no `class:` arg for caller-controlled wrapper classes; deferring until that lands. - `app/views/transactions/searches/filters/_badge.html.erb` — has a close button alongside the label (`button_to clear_filter_*`) and uses `rounded-3xl p-1.5` instead of a true pill. Closer to a removable filter chip — better fit for a separate `DS::FilterChip` primitive than for `DS::Pill`. Refs #1751. |
||
|
|
814505c5ea |
fix(settings/debugs): migrate in-cell <details> to DS::Disclosure :inline (#1916)
#1858's :inline variant landed (commit
|
||
|
|
ced133d06e |
fix(views): guard against nil entry.date in partials (#1878)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> |
||
|
|
548c4d1a3f |
fix(settings/debugs): replace 2 raw palette tokens flagged by DS drift scan (#1903)
`app/views/settings/debugs/show.html.erb` had two non-functional Tailwind classes flagged by sure-design's weekly merged-commit scan (#1895, #1898): - `bg-surface-default` → `bg-surface`. `bg-surface-default` doesn't map to any DS color variable (`--color-surface-default` isn't defined); `--color-surface` is the canonical token, auto-generates `bg-surface`. - `divide-gray-100` → `divide-alpha-black-200 theme-dark:divide-alpha-white-200`. Matches the existing pattern used by `admin/sso_providers/index.html.erb`, `admin/users/index.html.erb`, and `settings/preferences/show.html.erb` for tbody dividers. No `divide-primary` utility exists yet, so the bot's suggestion gets the same effect via the alpha tokens. The third drift finding on this file — the in-cell `<details>` metadata expander — is deferred until #1858's `DS::Disclosure :inline` variant lands on `main`. The `:default` variant renders a `bg-surface px-3 py-2 rounded-xl` card chrome that's wrong for an in-table-cell trigger; the `:inline` variant in #1858 is the right shape and will get a follow-up PR once that lands. Closes #1895 partially. Closes #1898 partially. Both bot issues stay open until the `<details>` migration also lands. |
||
|
|
8de14ed2a5 |
feat(design-system): DS::Disclosure :inline variant + migrate indexa_capital + snaptrade panels (#1715 §6) (#1858)
* feat(design-system): add :inline variant + migrate indexa_capital + snaptrade panels Adds an `:inline` variant to `DS::Disclosure` for plain text-link-style toggles that have no surface, no padding, no shadow — the disclosure reads as a clickable summary text + revealed content, nothing more. Use case: "Alternative auth" form section toggle in the Indexa Capital provider panel; "Manage connections" lazy-loaded toggle in the Snaptrade provider panel. Both were the last raw-`<details>` callsites in `app/views/settings/providers/`. Migrations: - `_indexa_capital_panel.html.erb` — single inline `<details>` revealing username / document / password form fields under an "Alternative auth" summary text. - `_snaptrade_panel.html.erb` — lazy-load `<details>` with `data-controller="lazy-load"` etc. The new `tag.details ... **opts` forwarding from #1857 lets the Stimulus controller attrs flow through cleanly via DS::Disclosure's `data:` keyword. Chevron rotation on snaptrade gets the standard `motion-safe:transition-transform motion-safe:duration-150` treatment (was `transition-transform` without the motion-safe gate). Variant summary now: | Variant | Details surface | Use case | |---|---|---| | `:default` | none / bg-surface summary | inline expander inside parent card | | `:card` | `bg-container shadow-border-xs rounded-xl p-4` | provider rows, settings sections | | `:card_inset` | `bg-surface-inset rounded-xl p-4` | inset sub-panels | | `:inline` | no surface | text-link-style toggles | * fix(review): guard variant.to_sym against nil in DS::Disclosure CodeRabbit on #1858 flagged that `variant: nil` crashed with `NoMethodError` at `variant.to_sym` before the explicit `VARIANTS` check could run. Use safe navigation (`variant&.to_sym`) so nil falls through to the validation, and inspect `@variant` in the error message so nil / non-symbol inputs render readably. Verified manually via runner: `DS::Disclosure.new(variant: nil)` now raises `ArgumentError: Invalid variant: nil. Must be one of [:default, :card, :card_inset, :inline]`. |
||
|
|
834ec19fdc |
feat(design-system): DS::Disclosure :card_inset variant + migrate ibkr_panel + settings/_section (#1715 §6) (#1857)
* feat(design-system): add :card_inset variant + migrate ibkr_panel and settings/_section Wraps up the disclosure migration cluster from #1715 §6: 1. **New `:card_inset` variant** on `DS::Disclosure`. Same contract as `:card` but uses `bg-surface-inset rounded-xl p-4` (no shadow) for inset sub-panels embedded inside a parent card surface. 2. **Migrate `_ibkr_panel.html.erb`** — the "flex query details" disclosure (`<details class="group bg-surface-inset rounded-xl p-4">`) was the one panel skipped from #1856 because it used the inset surface. Now uses `DS::Disclosure(variant: :card_inset)`. Chevron gets the `motion-safe:transition-transform motion-safe:duration-150` treatment along the way. 3. **Migrate `settings/_section.html.erb`** — the global "collapsible settings card" primitive backing 19 callsites via the `settings_section(...)` helper. The collapsible branch's `<details class="group bg-container shadow-border-xs rounded-xl p-4">` becomes `DS::Disclosure(variant: :card, open: open, data: ...)`. While here: - Update `disclosure.html.erb` to spread `**opts` onto the `<details>` element via `tag.details`. Previously opts were captured but never applied; the `settings/_section` migration needs `data-controller` + `data-auto-open-param-value` to flow through to the rendered `<details>`. - Non-collapsible branch in `settings/_section.html.erb` stays as raw `<section>` — different semantics (not expandable), DS::Disclosure can't replace because it always renders `<details>`. API: DS::Disclosure.new( variant: :card | :card_inset | :default, open: bool, data: { controller: "...", ... } # forwarded to <details> ) * fix(review): merge caller class in DS::Disclosure + i18n plaid deletion - DS::Disclosure: extract caller class: from opts and merge via class_names before forwarding to tag.details. Prevents the latent duplicate keyword arg error when callers pass class: alongside the variant-derived classes. - plaid_items/_plaid_item: localize "(deletion in progress...)" via t('.deletion_in_progress') + add en locale key, matching lunchflow / mercury / sophtron / coinstats convention. * fix(panels): replace text-white and bg-gray-tint-10 with semantic tokens `text-white` → `text-inverse` on the EnableBanking reauthorize button (`bg-warning` background); `bg-gray-tint-10` → `bg-container-inset` on the IndexaCapital item avatar wrapper. Both flagged by sure-design as non-functional palette tokens. Pre-existing on main; surfaced by the re-indentation that this PR applied during the disclosure migration. |
||
|
|
547dd21bf2 |
feat(design-system): migrate 3 provider panels to DS::Disclosure :card (#1715 §6) (#1856)
* feat(design-system): DS::Disclosure :card variant + migrate 14 provider items Resolves part of #1715 §6. The provider-item view templates (binance, brex, coinbase, coinstats, enable_banking, ibkr, indexa_capital, kraken, lunchflow, mercury, plaid, simplefin, snaptrade, sophtron — 14 in total) all hand-rolled the same `<details open class="group bg-container p-4 shadow-border-xs rounded-xl">` shell with a custom summary inside and content below. Extend `DS::Disclosure` with a `:card` variant that bakes the card chrome onto the `<details>` element itself; the summary becomes slot-driven via the existing `summary_content` slot. Provider items keep their custom summary content (logos, brand colors, status copy) unchanged — they just hand it to the slot instead of writing it between `<summary>` tags. API: DS::Disclosure.new(variant: :card, open: true) do |d| d.with_summary_content do <div class="flex items-center gap-2"> chevron + custom summary markup </div> end body content end While here: - Drop the no-op `group-open:transform` from the default chevron (Tailwind v4 applies `rotate-90` directly). - Add `motion-safe:transition-transform motion-safe:duration-150` to chevron rotation for reduced-motion respect (matches the pattern landing in #1841). - Extract `summary_classes` / `details_classes` helpers so the default and card surfaces stay readable side-by-side. Note: this PR touches `DS::Disclosure` and will textually conflict with #1841 (focus-ring + reduced-motion polish). Both changes are compatible — when #1841 merges first, the resolution is just preserving both edits (the focus-ring classes are already merged into `summary_classes` here). * feat(design-system): migrate 3 provider panels to DS::Disclosure :card variant Resolves the panel slice of #1715 §6. Continuation of the DS::Disclosure :card variant work — same migration pattern, applied to the 3 provider-PANEL templates that share the card shape with the provider-item templates landing on the parent branch. Migrated `<details class="group bg-container p-4 shadow-border-xs rounded-xl">` → `DS::Disclosure.new(variant: :card)` in: - `app/views/settings/providers/_kraken_panel.html.erb` — 1 details in the items-each loop. - `app/views/settings/providers/_mercury_panel.html.erb` — 1 details in the items-each loop. - `app/views/settings/providers/_brex_panel.html.erb` — 2 details: one in the items-each loop, one standalone "add connection" panel that opened by default when no active items existed. The conditional `<%= "open" unless active_items.any? %>` becomes `open: active_items.none?` on the `:card` disclosure. Panels do NOT show a chevron in their summary (different UX from the per-item rows in #1855), so the migration preserves that — no chevron inserted. NOT migrated (intentionally — different shapes): - `_ibkr_panel.html.erb` — `<details class="group bg-surface-inset rounded-xl p-4">`. Uses bg-surface-inset, not bg-container — needs a `:card-inset` variant we haven't built. Deferred. - `_indexa_capital_panel.html.erb` — `<details class="group">` with no card chrome. Inline expander; doesn't fit either disclosure variant. - `_snaptrade_panel.html.erb` — same inline pattern as indexa_capital. * fix(review): use ring-alpha-black-300 focus token in DS::Disclosure CodeRabbit P2: switch the focus-visible outline from raw gray-900/white palette values to the alpha-black-300 ring token, matching the established focus pattern on settings/provider_card.html.erb. This keeps theme behavior centralized in the design system tokens instead of branching on theme-dark: in the component. Applies to both :default and :card summary variants. * fix(review): stretch DS::Disclosure summary_content to full width Codex P2 follow-up on the disclosure-migration stack: \`<summary>\` is \`display: list-item\`, so a flex inner div inside the slot shrink-wraps to content width — any \`justify-between\` the caller adds has nothing to distribute, and the right-side admin actions collapse toward the title across every provider-item partial migrated to \`DS::Disclosure variant: :card\` in #1855 (and the panels in #1856 / #1857 / #1858 that inherit this component). Wrap the slot in \`<div class=\"w-full\">\` so caller-supplied flex rows stretch across the card. \`:default\` variant is unchanged (it never uses \`summary_content\`). * fix(review): stretch :card summary flex row to full width Codex P2 follow-up on #1856: the migrated kraken / mercury / brex panel summary rows wrap their content in \`<div class=\"flex items-center justify-between gap-X\">\`, but a flex container inside \`<summary>\` (\`display: list-item\`) shrink-wraps to content size, so \`justify-between\` had nothing to distribute and the right-side admin actions collapsed toward the title. Add \`w-full\` so the flex row stretches across the card. The deeper component-level fix lands in #1855 (wraps \`summary_content\` in a \`w-full\` block); this commit makes #1856 self-contained against the merge order. |
||
|
|
78c3331360 |
feat(design-system): DS::Disclosure :card variant + migrate 14 provider items (#1715 §6) (#1855)
* feat(design-system): DS::Disclosure :card variant + migrate 14 provider items Resolves part of #1715 §6. The provider-item view templates (binance, brex, coinbase, coinstats, enable_banking, ibkr, indexa_capital, kraken, lunchflow, mercury, plaid, simplefin, snaptrade, sophtron — 14 in total) all hand-rolled the same `<details open class="group bg-container p-4 shadow-border-xs rounded-xl">` shell with a custom summary inside and content below. Extend `DS::Disclosure` with a `:card` variant that bakes the card chrome onto the `<details>` element itself; the summary becomes slot-driven via the existing `summary_content` slot. Provider items keep their custom summary content (logos, brand colors, status copy) unchanged — they just hand it to the slot instead of writing it between `<summary>` tags. API: DS::Disclosure.new(variant: :card, open: true) do |d| d.with_summary_content do <div class="flex items-center gap-2"> chevron + custom summary markup </div> end body content end While here: - Drop the no-op `group-open:transform` from the default chevron (Tailwind v4 applies `rotate-90` directly). - Add `motion-safe:transition-transform motion-safe:duration-150` to chevron rotation for reduced-motion respect (matches the pattern landing in #1841). - Extract `summary_classes` / `details_classes` helpers so the default and card surfaces stay readable side-by-side. Note: this PR touches `DS::Disclosure` and will textually conflict with #1841 (focus-ring + reduced-motion polish). Both changes are compatible — when #1841 merges first, the resolution is just preserving both edits (the focus-ring classes are already merged into `summary_classes` here). * fix(review): use ring-alpha-black-300 focus token in DS::Disclosure CodeRabbit P2: switch the focus-visible outline from raw gray-900/white palette values to the alpha-black-300 ring token, matching the established focus pattern on settings/provider_card.html.erb. This keeps theme behavior centralized in the design system tokens instead of branching on theme-dark: in the component. Applies to both :default and :card summary variants. * fix(review): stretch DS::Disclosure summary_content to full width Codex P2 follow-up on the disclosure-migration stack: \`<summary>\` is \`display: list-item\`, so a flex inner div inside the slot shrink-wraps to content width — any \`justify-between\` the caller adds has nothing to distribute, and the right-side admin actions collapse toward the title across every provider-item partial migrated to \`DS::Disclosure variant: :card\` in #1855 (and the panels in #1856 / #1857 / #1858 that inherit this component). Wrap the slot in \`<div class=\"w-full\">\` so caller-supplied flex rows stretch across the card. \`:default\` variant is unchanged (it never uses \`summary_content\`). |
||
|
|
8e444ff98b |
feat(design-system): add DS::SearchInput primitive (closes #1715 §3) (#1853)
* feat(design-system): add DS::SearchInput + migrate 2 broken-focus callsites Resolves #1715 §3. Two standalone search-field callsites — `/settings/preferences` currency filter and `/settings/providers` filter row — had a hand- rolled markup that ended in `focus:ring-gray-500`. That utility has no backing token in the design system (`ring-gray-500` isn't in Tailwind's default + Sure doesn't register a gray ring color), so the input rendered with zero focus indicator on a bordered bg-container surface. Keyboard users couldn't tell when the field was focused. Introduce `DS::SearchInput` — icon-on-left, bordered, token-backed focus ring matching the DS::Button pattern landing in #1840 (`outline-2 outline-offset-2 outline-gray-900` with the dark-mode override). API: DS::SearchInput.new( name: "...", placeholder: "...", value: ..., aria_label: "...", # defaults to placeholder class: "...", # passed to the wrapper **opts # spread onto the <input>, e.g. data-* ) Migrate the two broken callsites. Three other "search" patterns stay as-is (out of scope for this PR): - `form.search_field :search` inside `styled_form_with` blocks (accounts/show/_activity.html.erb, UI::Account::ActivityFeed) — already routes through StyledFormBuilder's form-field CSS. - Embedded-dropdown search input inside DS::Select, DS::Menu, and the splits/category-select panels — uses a different shape (no border, no ring) because the parent panel provides the chrome. - Category dropdown's combobox search input (app/views/category/dropdowns/show.html.erb) — has a custom `role=combobox` flow and stays intentionally distinct. * feat(design-system): add embedded variant to DS::SearchInput, migrate 2 more callsites Adds `variant: :embedded` to `DS::SearchInput` for search inputs that live *inside* another DS panel (DS::Select dropdown, splits category filter, future DS::Popover-hosted filters). No own border / no own focus ring — the parent panel provides the chrome, so adding ring + outline competes with its `focus-within` state. API: DS::SearchInput.new(variant: :embedded, placeholder: "...", data: {...}) The `:standalone` default (from the previous commit) stays unchanged and remains the right choice for top-of-list filter inputs. Migrated: - `app/components/DS/select.html.erb` — the in-dropdown search input for `DS::Select.new(searchable: true)`. Was the only remaining internal raw <input type="search"> markup in the component. - `app/views/splits/_category_select.html.erb` — split-transaction category picker filter. Same shape as DS::Select's search but hand-rolled because the picker isn't a vanilla DS::Select. Three other search patterns stay out of scope (intentionally, per the previous commit): - `form.search_field :search` inside `styled_form_with` — uses form-field CSS, different visual contract. - `app/views/category/dropdowns/show.html.erb` — bespoke `role="combobox"` flow with `aria-expanded` / `aria-autocomplete` semantics that don't belong in this primitive. * fix(review): mobile font + embedded variant focus-within ring - DS::SearchInput: switch text-sm -> text-base sm:text-sm on both variants so the input keeps its 16px base size on mobile. iOS Safari zooms the viewport when a focused input is below 16px, which the unconditional text-sm was triggering on the Settings Preferences currency search and Settings Bank Sync provider search. - DS::Select (searchable variant) + splits/_category_select: add focus-within:ring-4 focus-within:ring-alpha-black-200 (with theme-dark variant) on the wrapper around the embedded search input. The embedded variant intentionally has no own focus ring so it inherits chrome from its parent panel — but the two current parent panels were not providing one, so keyboard focus on the dropdown search box rendered with no visible indicator. Ring matches the .form-field token used across the design system. * fix(merge): repair DS::Select search input merge resolution The previous merge of main left invalid Ruby inside the DS::SearchInput `data:` hash: aria-label="<%= t("helpers.select.search_placeholder") %>" This is an ERB string assignment masquerading as a hash entry — it does not parse and would have raised SyntaxError at render. Two follow-ups: - Drop the `aria-label` entry entirely. `DS::SearchInput` already defaults `aria_label` to `placeholder`, and `placeholder` is set on the call, so the resulting <input> already carries `aria-label="<%= t(...) %>"`. - Restore the `input->select#syncTabindex` action that main #1848 added on the embedded search input. It keeps the roving tabindex on the listbox in sync as filtered results change. Original PR branch had only `list-filter#filter`; reintegrate both with explicit `input->` event prefixes for parity with main. --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
655895341d |
feat(imports): verify Sure NDJSON import readback (#1869)
* feat(imports): verify Sure NDJSON readback * fix(imports): tighten Sure readback verification * fix(imports): polish Sure verification review nits |
||
|
|
e21ab9819f |
feat(dashboard): zoom into cashflow sankey categories (#1807)
* 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> |
||
|
|
12785754c8 |
feat(design-system): split DS::Menu into strict action-list + new DS::Popover (#1850)
* feat(design-system): split DS::Menu into strict action-list + new DS::Popover for mixed content Closes #1743. DS::Menu used to absorb both action-list dropdowns (row context menus, "more actions") AND mixed-content panels (user-account dropdown, filter forms, picker pop-ups). The two shapes carry incompatible a11y contracts: - **Action list**: `role="menu"` container, `role="menuitem"` children, Up/Down arrow nav per WAI-ARIA APG. - **Mixed content**: NO menu role — `role="menu"` restricts AT users to menuitem-only navigation and breaks any panel with forms, headings, or generic groupings. This PR splits the component: ## DS::Menu (tightened) Strict action-list primitive. Variants reduced to `:icon` and `:button` (no `:avatar`). `custom_content` slot removed. Bakes in: - `role="menu"` on the panel, `aria-haspopup="menu"` + `aria-expanded` + `aria-controls` on the trigger. - `role="menuitem"` + `tabindex="-1"` on every DS::MenuItem; the controller installs roving tabindex (first item gets `tabindex="0"` when the menu opens) and handles ArrowUp/Down/Home/End + Escape + Enter/Space activation. - `role="separator"` on the divider variant. - Stable per-instance `menu-<8-char hex>` id so the trigger's `aria-controls` resolves correctly. `DS::Menu.new(variant: :avatar, ...)` now raises ArgumentError pointing at DS::Popover. ## DS::Popover (new) Positioned panel for **mixed**, **non-action-list** content: account menus, picker forms, filter forms, embedded controls. Slots: `button`, `header`, `custom_content`. Variants: `:icon`, `:button`, `:avatar`. NO `role="menu"` — the panel announces as a generic dialog-popup (`aria-haspopup="dialog"`, `aria-expanded`, `aria-controls`). Mirrors DS::Menu's floating-ui positioning + Escape/outside-click lifecycle in its own Stimulus controller (`DS--popover`). Avatar variant ships a focus ring + bumped touch target (44×44 via `w-11 h-11` per #1738). ## Migrated callsites (7 → DS::Popover) - `app/views/users/_user_menu.html.erb` — avatar trigger + profile header + nav links (items kept as DS::MenuItem inside `custom_content` for visual parity) - `app/views/categories/_menu.html.erb` — turbo-framed category picker - `app/views/budgets/_budget_header.html.erb` — budget picker - `app/views/reports/index.html.erb` — period picker - `app/views/holdings/_cost_basis_cell.html.erb` — cost-basis edit form - `app/views/transactions/searches/_form.html.erb` — filter form - `app/components/UI/account/activity_feed.html.erb:70` — status checkboxes (the row-level "new" menu on line 9 stays as DS::Menu) The other 33 DS::Menu callsites stay as-is — pure action lists. Locale: `ds.popover.avatar_default_label` + `users.user_menu.aria_label` keys added (en only; other locales handled in a separate i18n pass). * fix(test): update sidebar user-menu selector for Menu→Popover migration The user-menu now renders as `DS::Popover` (variant: :avatar) instead of `DS::Menu` after the menu split, so its trigger carries `data-DS--popover-target="button"` rather than the old `data-DS--menu-target`. Update the sidebar-driven settings test helper to match — every system test that drives Settings via the sidebar gates on this selector. * fix(review): DS::Popover/Menu trigger a11y + caller-attr preservation - popover.rb / menu.rb: button slot now merges (not overwrites) caller- provided data and aria hashes, sets aria-haspopup/expanded/controls on the :button variant, defaults type="button" on block-rendered buttons. - menu.rb / menu.html.erb: drop renders_one :header (strict-menu API shouldn't expose an arbitrary-markup escape hatch); preview updated. - menu_controller.js: handle Enter/Space activation on focused menuitem so keyboard navigation matches the ARIA menu pattern. - cost_basis_cell / transactions/searches/_menu: retarget cancel button data-action from DS--menu#close to DS--popover#close (host controller changed in the migration). * fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai> * fix(review): MenuItem roving: false for DS::Popover usage Codex P1 on #1850: \`DS::MenuItem\` hard-codes \`tabindex=\"-1\"\` and \`role=\"menuitem\"\` for both link and button variants — correct inside \`DS::Menu\` (which provides arrow-key roving and announces \`role=\"menu\"\`), but breaks every \`DS::MenuItem\` rendered inside \`DS::Popover\` (\`app/views/users/_user_menu.html.erb\`). Popover has no roving handler, so Tab skips every item — Settings, Changelog, Feedback, Contact, Log out become keyboard-unreachable. Add a \`roving:\` keyword (default \`true\`) to \`DS::MenuItem\` that gates both \`tabindex=\"-1\"\` and \`role=\"menuitem\"\`. \`DS::Menu\` callers keep the default (roving menu semantics intact). Pass \`roving: false\` from \`_user_menu.html.erb\` so user-menu items land in the normal Tab order. Existing \`menu.with_item(...)\` callers in the design system still default to \`true\`, so no behavior change for \`DS::Menu\` consumers. * fix(review): make menuitem_attrs authoritative on roving CodeRabbit Major on #1850: \`merged_opts\` was splatted AFTER \`menuitem_attrs\` in \`DS::MenuItem#wrapper\`, so a stray \`role: :button\` or \`tabindex: 0\` from a \`menu.with_item(..., role: …)\` caller could silently downgrade the \`DS::Menu\` ARIA contract that \`menuitem_attrs\` enforces. Strip \`:role\` and \`:tabindex\` from \`merged_opts\` whenever \`roving\` is enabled, then splat \`menuitem_attrs\` last. When \`roving: false\` (popover usage in \`_user_menu.html.erb\`) callers keep full control — Tab order and explicit ARIA stay tunable by the caller. --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <noreply@coderabbit.ai> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
355648c4a6 |
refactor(design-system): migrate orphan btn-- buttons to DS::Button / DS::Link (#1715 §5 part A) (#1859)
* refactor(design-system): migrate 9 hand-rolled buttons with orphan btn-- classes to DS::Button / DS::Link Part of #1715 §5. The `btn`, `btn--primary`, `btn--outline`, `btn--ghost`, `btn--sm` CSS classes have no backing styles anywhere in the codebase (no .btn definition in app/assets/, no Bootstrap dependency). These callsites have been rendering unstyled buttons / links since the underlying CSS was last removed. Migrate the 9 broken callsites: - `app/views/transactions/show.html.erb` — duplicate-merge action buttons (×2): `button_to ... class: "btn btn--primary btn--sm"` / `class: "btn btn--outline btn--sm"` → DS::Button with href + variant + size + `data: { turbo_method: :post }`. - `app/views/snaptrade_items/select_existing_account.html.erb` — "Go to Provider Settings" link → DS::Link primary sm. - `app/views/indexa_capital_items/select_existing_account.html.erb` — same pattern → DS::Link primary sm. - `app/views/import/confirms/show.html.erb` — Publish button + Cancel link → DS::Button primary full-width + DS::Link ghost full-width. - `app/views/simplefin_items/new.html.erb` — Cancel link (`class: "btn"` only) + Connect submit → DS::Link secondary + bare `f.submit` (already routes to DS::Button via StyledFormBuilder). - `app/views/settings/providers/_ibkr_panel.html.erb`, `_snaptrade_panel.html.erb`, `_indexa_capital_panel.html.erb` — strip the orphan `class: "btn btn--primary"` from `f.submit` callers; the submit is already a styled DS::Button via the form builder. The next PR in this chain (Phase B) will tackle the larger inline- button cluster (~29 files, 38 instances) — provider panels and provider-item flows hand-rolling the same `inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-inverse bg-inverse hover:bg-inverse-hover focus:outline-none focus:ring-2 focus:ring-primary transition-colors` string. * fix(review): render DS::Button for unstyled submits in PR #1859 - simplefin_items/new.html.erb uses plain form_with (not styled_form_with), so f.submit was rendering a bare browser submit input. Render DS::Button with type: :submit explicitly. - _indexa_capital_panel.html.erb already uses styled_form_with; strip the orphan Tailwind class string from f.submit so StyledFormBuilder fully owns the DS::Button styling (matches the IBKR and SnapTrade panel pattern). Addresses Codex and CodeRabbit feedback on #1859. --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
e67ff3e3dc |
refactor(design-system): migrate single-color tokens to @theme + lint @utility /N footgun (#1849)
* refactor(design-system): migrate single-color semantic tokens to @theme + lint @utility /N footgun Closes #1653. Tailwind v4 auto-generates the `/N` opacity-modifier pipeline (`color-mix(in oklab, var(--color-X) N%, transparent)`) only for colors declared in `@theme`. Tokens emitted as `@utility name { @apply ... }` bypass that pipeline entirely, so `text-link/70`, `bg-surface/50`, etc. silently compile to nothing — the workaround from #1626 was `text-inverse opacity-70`. Migrate the 11 single-color semantic tokens whose class names match Tailwind's color-utility convention (`bg-X`, `text-X`, `border-X`) and have no cross-prefix collision: bg-surface, bg-surface-hover, bg-surface-inset, bg-surface-inset-hover bg-container, bg-container-hover, bg-container-inset, bg-container-inset-hover bg-nav-indicator text-link border-tertiary After migration, `--color-surface`, `--color-container`, etc. live in `@theme` and Tailwind auto-generates every prefix variant (`bg-surface`, `text-surface`, `border-surface`, plus `/10`..`/100`). The original utility class names are preserved (now via auto-generation instead of `@utility` blocks), so every existing callsite continues to work. NOT migrated, by design: - **inverse family** (`bg-inverse`, `text-inverse`, `bg-inverse-hover`, `border-inverse`): bg- and text- variants have *different* colors, cannot share one `--color-inverse`. Renaming the family (`bg-strong-surface` + `text-on-strong-surface`) would touch ~61 view files and trade one footgun for semantic loss; deferred until a concrete `bg-inverse/N` use case appears. - **primary/secondary/subdued/destructive** (cross-prefix collision): `text-primary` (gray.900) and `border-primary` (alpha-black.300) carry deliberately distinct values, can't share `--color-primary`. Same for the secondary/subdued pairs. Migrating either alone would force a rename of the other. - **button-bg-*, tab-item-*, tab-bg-group**: class names don't follow Tailwind's `<prefix>-<name>` convention, so auto-generation would emit `bg-button-bg-primary` not `button-bg-primary`. - **composites** (`bg-loader`, `bg-overlay`, `shadow-border-*`, `border-divider`): compile to multiple properties or alias-reference other utilities — must stay as @utility. Add an `erb_lint` DeprecatedClasses rule covering the @utility-only tokens with `\d+` regex modifiers so any future `text-inverse/70` etc. fails CI with the explanation that `opacity-N` is the workaround and #1653 is the tracking issue. Verified the rule fires on synthetic input; verified zero new violations on the existing app. Stats: `@utility` blocks dropped from 45 → 34; @theme primitives grew from 183 → 194. * fix(review): cover remaining @utility /N footgun tokens in erb_lint CodeRabbit flagged that the new DeprecatedClasses /N rule missed seven still-defined @utility color tokens: border-destructive, border-solid, button-bg-secondary-strong, button-bg-secondary-strong-hover, button-bg-disabled, button-bg-ghost-hover, button-bg-outline-hover. Without them, classes like button-bg-disabled/50 pass lint while Tailwind silently drops the class. Adding the patterns surfaced two pre-existing offenders (border-destructive/30, border-destructive/20). Swap both to solid border-destructive — the @utility override defines red-500 (light) while --color-destructive in @theme is red-600, so the /N modifier was rendering an off-shade rather than the intended faded variant. Verified the rule fires on synthetic input for all seven new patterns, then verified zero remaining violations on the new patterns across app/**/*.erb. * chore(erb_lint): add trailing newline to .erb_lint.yml Per review feedback on #1849. Some editors flag the missing newline; keeps style consistent with the rest of the codebase. |
||
|
|
e30ccd94af |
fix(design-system): DS::Tooltip a11y — focusable trigger, keyboard parity, Esc dismiss (#1845)
* fix(design-system): DS::Tooltip a11y — focusable trigger, keyboard parity, Esc dismiss Closes #1747. Five fixes on the tooltip primitive. 1. **Tooltip anchor not in a11y tree.** The trigger was a bare Lucide icon, which Lucide renders with `aria-hidden="true"`. The tooltip target had `role="tooltip"` but nothing referenced it, so AT users had no way to discover the description. Wrap the icon in a focusable `<button type="button">` with `aria-describedby="<tooltip-id>"` so the underlying icon stays `aria-hidden` and the button picks up the description binding. 2. **Stable per-instance id.** Each DS::Tooltip now mints a `tooltip-<8-char hex>` id wired between the trigger's `aria-describedby` and the tooltip's `id`. 3. **Keyboard parity.** Hover-only triggers locked keyboard-only users out. Add `focusin` / `focusout` listeners on the controller element so Tab onto the trigger reveals the tooltip, Tab away dismisses it. 4. **Esc-to-dismiss.** Matches the WAI-ARIA tooltip pattern. `Escape` while the tooltip is open closes it without removing focus from the trigger. 5. **Resize-safe width cap.** Replace the hard-coded `max-w-[200px]` with `max-w-[20rem]` so the tooltip scales with the user's root font-size setting (large-text accessibility pref). Slightly wider visual cap (320px @ default) but no longer clips on text-zoom. Plus: docstring note that tooltip content must be non-interactive (no buttons / links / form controls inside) — `aria-describedby` exposes content as a description, not as an interactive subtree. Callers needing actions should reach for a popover/menu primitive. API unchanged. Existing 30+ DS::Tooltip callsites work without modification — they all pass `text:`-only payloads, which still render correctly under the new markup. * fix(review): as: option + alpha focus-ring on DS::Tooltip Addresses two AI review findings on #1845: 1. **Button-inside-summary spec violation.** Wrapping the icon in `<button>` regressed keyboard/AT behavior at 13 callsites where DS::Tooltip lives inside a `<summary>` (8 provider items, lunchflow disclosure, activity_date, 4 simplefin badges). HTML's content model forbids interactive content inside `<summary>`; browsers and AT can drop focus or conflate activation with the disclosure toggle. Add `as:` parameter — default `:button` preserves the standalone a11y wrap; `:span` renders a non-focusable wrapper for summary-nested usage. `focusin` bubbles up to the controller from the ancestor `<summary>`, so keyboard tooltips still appear on tab. Migrate the 13 in-summary callsites to `as: :span`. 2. **Raw palette focus ring → alpha tokens.** Swap `outline-gray-900 theme-dark:focus-visible:outline-white` to the established focus-ring pattern `focus-visible:ring-2 focus-visible:ring-alpha-black-300 theme-dark:focus-visible:ring-alpha-white-300` — matches the DS::Toggle fix landed in #1843 review and provider_card / form-field tokens. * fix(review): bind tooltip focus on ancestor <summary> Codex P2 follow-up on #1845: \`as: :span\` renders a non-focusable trigger inside the disclosure \`<summary>\`. Keyboard users hit Tab and focus lands on the summary itself; \`focusin\` fires on the summary and bubbles UP — never down to a descendant span — so the existing listener on \`this.element\` never fires and the tooltip stays hidden for keyboard-only users on every in-summary row (provider _item partials, lunchflow disclosure, activity_date, simplefin badges). My earlier reply that the focusin "bubbles up to the Stimulus controller on the outer span" was wrong about the direction; \`focusin\` only bubbles upward. In \`addEventListeners\`, resolve \`this.element.closest("summary")\` and bind \`focusin\` / \`focusout\` / \`keydown\` on it too. Track the ancestor on the controller and undo the bindings in \`removeEventListeners\` so reconnect-on-Turbo cycles don't leak. Update the template comment to reflect the actual mechanism. * docs(ds-tooltip): correct as=:span comment to match controller mechanism --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
cdce00c71e |
refactor(design-system): migrate 38 hand-rolled provider buttons to DS::Button / DS::Link (#1715 §5 part B) (#1860)
* refactor(design-system): migrate 9 hand-rolled buttons with orphan btn-- classes to DS::Button / DS::Link Part of #1715 §5. The `btn`, `btn--primary`, `btn--outline`, `btn--ghost`, `btn--sm` CSS classes have no backing styles anywhere in the codebase (no .btn definition in app/assets/, no Bootstrap dependency). These callsites have been rendering unstyled buttons / links since the underlying CSS was last removed. Migrate the 9 broken callsites: - `app/views/transactions/show.html.erb` — duplicate-merge action buttons (×2): `button_to ... class: "btn btn--primary btn--sm"` / `class: "btn btn--outline btn--sm"` → DS::Button with href + variant + size + `data: { turbo_method: :post }`. - `app/views/snaptrade_items/select_existing_account.html.erb` — "Go to Provider Settings" link → DS::Link primary sm. - `app/views/indexa_capital_items/select_existing_account.html.erb` — same pattern → DS::Link primary sm. - `app/views/import/confirms/show.html.erb` — Publish button + Cancel link → DS::Button primary full-width + DS::Link ghost full-width. - `app/views/simplefin_items/new.html.erb` — Cancel link (`class: "btn"` only) + Connect submit → DS::Link secondary + bare `f.submit` (already routes to DS::Button via StyledFormBuilder). - `app/views/settings/providers/_ibkr_panel.html.erb`, `_snaptrade_panel.html.erb`, `_indexa_capital_panel.html.erb` — strip the orphan `class: "btn btn--primary"` from `f.submit` callers; the submit is already a styled DS::Button via the form builder. The next PR in this chain (Phase B) will tackle the larger inline- button cluster (~29 files, 38 instances) — provider panels and provider-item flows hand-rolling the same `inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-inverse bg-inverse hover:bg-inverse-hover focus:outline-none focus:ring-2 focus:ring-primary transition-colors` string. * refactor(design-system): migrate 38 hand-rolled provider buttons to DS::Button / DS::Link (#1715 §5 part B) Bulk sweep of the second cluster from §5. 29 files, 38 button instances — each one hand-rolled the same long Tailwind string for the primary action button: inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-inverse bg-inverse hover:bg-inverse-hover focus:outline-none focus:ring-2 focus:ring-primary transition-colors (some variations used `button-bg-primary hover:button-bg-primary-hover` instead of `bg-inverse hover:bg-inverse-hover` — same intent). Every instance is now a DS::Button / DS::Link with `variant: :primary`, which: - Picks up the new focus-ring + touch-target work from #1840 once that merges. - Stops duplicating the long Tailwind string across 29 files — single source of truth in `DS::Buttonish::VARIANTS[:primary]`. - Picks up consistent `aria-label` derivation for icon-only forms. - Removes the misnamed `focus:ring-primary` (no token) — the new ring comes from `base.css` automatically. Migration patterns applied: - `f.submit text, class: "inline-flex …"` inside `styled_form_with` → bare `<%= f.submit text %>`. StyledFormBuilder routes through DS::Button. - `link_to text, path, class: "inline-flex …"` → DS::Link primary. - `button_to text, path, method: :X, class: "inline-flex …"` → DS::Button with `href: path` and `data: { turbo_method: :X }`. - `submit_tag text, class: "inline-flex …"` inside raw `form_with` → DS::Button with `type: :submit`. Notable adjustments: - `holdings/show.html.erb` — the form was `form_with` (not styled). Switched to `styled_form_with` so `f.submit` routes through DS::Button. `f.combobox` (hotwire_combobox) still works through the styled builder. - Two `link_to settings_providers_path` callsites in `coinstats_items/new.html.erb` + `enable_banking_items/new.html.erb` had `w-full inline-flex … hidden md:inline-flex` — the responsive pair conflicted (both `inline-flex` and `hidden md:inline-flex` on the same element). Migrated to `full_width: true` without the responsive split; the buttons now render at all breakpoints consistently. (Pre-existing copy-paste bug, fixed in passing.) - `enable_banking_panel` add-connection button gained `icon: "plus"` via the DS::Button API; the explicit `gap-2 … icon "plus"` markup is now redundant. Sibling buttons that don't match the primary spec (destructive trash, secondary outline-bordered, button-bg-secondary-strong on holdings/show.html.erb, etc.) are intentionally left alone — they need their own audit pass once #1840 lands and the focus-ring behavior on those variants is stable. * fix(review): restore SimpleFIN submit styling + i18n provider_form label - SimpleFIN new modal: switch form_with -> styled_form_with so f.submit picks up the DS::Button render via styled builder (Codex #1860). - _provider_form: replace hardcoded "Save and connect" with t(".save_and_connect") and add scoped key under settings.providers.provider_form (CodeRabbit). |
||
|
|
e07d641ead |
fix(design-system): DS::Button a11y audit — focus ring, touch target, type default, icon-only label (#1840)
* fix(design-system): DS::Button a11y audit Closes #1738. Four concrete fixes surfaced by the savings-goals audit + #1737 universal checklist: 1. Focus ring (WCAG 2.4.7). `base.css` had `focus-visible:outline-gray-900` which is **1.07:1** against the primary button's gray-900 background — invisible. Widen to `outline-2 outline-offset-2`, place outline outside the button via offset, and add a dark-mode `outline-white` so the ring is always visible against the page chrome regardless of the button surface. 2. Touch target (WCAG 2.5.5). Icon-only buttons at the default `:md` size were `w-9 h-9` = 36×36, below the 44×44 enhanced target. Bump `md.icon_container_classes` to `w-11 h-11` and `lg.icon_container_classes` to `w-12 h-12` to keep the size scale intact. `sm` stays at 32×32 (already passes WCAG 2.5.8 AA's 24×24 minimum; intentional compact-density variant). 3. Default button type. `content_tag(:button, ...)` inherits the HTML default `type="submit"`, so a DS::Button rendered inside a form steals Enter-key submission from the first text input (reproducible in the form stepper). Default to `type="button"` in the non-`href` branch; existing form submitters pass `type: "submit"` explicitly and continue to work. The `button_to` (href) branch keeps the submit default because button_to wraps its own form. 4. Icon-only accessible name. Icon-only buttons render no text node, so AT users hear "button" with no name. Derive a humanized aria-label from the icon key (e.g. `icon: "more-horizontal"` → `aria-label="More horizontal"`); explicit `aria: { label: }` on the caller still wins. Soft fallback — callers should still pass meaningful labels for richer copy. Plus: replace the stale `fg-white` icon class on the destructive variant with `text-inverse` (the `fg-*` namespace was deprecated in #1626 so `fg-white` resolved to nothing; the icon was using its helper-default color rather than the white the design intended). Out of scope: - Menu avatar trigger (custom 36×36 button bypassing DS::Button) — belongs to #1743 DS::Menu audit. - DS::FilledIcon `lg` size container (decorative, not interactive) — belongs to #1742. * fix(design-system): force type=submit on StyledFormBuilder#submit The DS::Button default-type-button change in the previous commit broke every `form.submit "Log in"` callsite because `StyledFormBuilder#submit` (app/helpers/styled_form_builder.rb) renders a DS::Button under the hood with no explicit `type:`. After the default flip, those submit buttons rendered as `type="button"`, so submitting forms (login, password reset, every form using `form.submit`) silently no-ops. CI surfaced this via ~30 system tests failing in the `sign_in` helper, which couldn't get past the login page. Pin `type: "submit"` on the DS::Button rendered by `StyledFormBuilder#submit`. The 22 view-level `f.submit` / `render DS::Button.new(type: :submit, ...)` callers already pass type explicitly and are unaffected. * fix(review): href-branch type-button bug + focus-ring tokens + profile Save submit CodeRabbit P1+P2 review on #1840: 1. button.rb: `merged_opts.delete(:href)` always returned nil because Buttonish#initialize strips :href from opts into @href, so the `if href.blank?` guard was ALWAYS true. Every DS::Button rendered via button_to (the href branch) got `type="button"` on the inner button, breaking submission of those button_to-generated forms (e.g. imports/_ready.html.erb publish button, imports/_failure.html.erb try-again button). Drop the local `href = merged_opts.delete(:href)` so the guard now reads the @href reader, leaving the href branch's HTML default intact. 2. settings/profiles/show.html.erb: the Save button is rendered with `render DS::Button.new(...)` inside `styled_form_with` (not via form.submit), so the StyledFormBuilder#submit type-pin from |
||
|
|
e8ce28648d |
refactor: rename beta features gate to preview features (#1837)
* refactor: rename beta features gate to preview features Renames the opt-in gate introduced in PR #1829 from "beta" to "preview". Same shape (per-user JSONB toggle, `before_action` concern, marker pill) just retitled so the surface speaks the language Sure uses elsewhere ("preview" reads as in-progress, "beta" had baggage with provider maturity copy and external testing programs). Renames: - BetaGateable -> PreviewGateable - require_beta_features! -> require_preview_features! - beta_features_enabled? -> preview_features_enabled? - preferences["beta_features_enabled"] -> preferences["preview_features_enabled"] - DS::Pill default label "Beta" -> "Preview" - Settings -> Preferences toggle copy "beta features" -> "preview features" - config/locales/views/beta/ -> config/locales/views/preview/ - docs/llm-guides/gating-a-beta-feature.md -> gating-a-preview-feature.md Includes a data migration that copies any existing `beta_features_enabled` JSONB key into `preview_features_enabled` so early opt-ins survive the rename, then removes the old key. The migration is fully reversible. Provider maturity copy ("maturity.beta = Beta" under Settings -> Bank sync) is intentionally untouched - that's a separate concept describing a provider's integration stability, not Sure's feature gate. * review: apply CodeRabbit findings on PR #1837 - Settings::PreferencesController#update now routes the `preview_features_enabled` input through strong params and casts via ActiveModel::Type::Boolean instead of reading raw params and string- comparing to "1". Matches Sure's controller convention for permitted params and avoids stringly-typed boolean handling. - Rename migration now wraps the destination JSONB key write in COALESCE so a row that somehow ends up with both keys keeps the destination value instead of having it overwritten by the source. Up and down paths get the same defensive shape. * 📝 CodeRabbit Chat: Implement requested code changes * 📝 CodeRabbit Chat: Implement requested code changes * fix: restore all missing translation keys; rename beta→preview label * fix: restore all missing sections (appearances, debugs, llm_usages, providers, etc.); rename beta→preview * fix: restore missing keys (member_removal_failed, confirm_delete, etc.); add preview section * fix(i18n/ca): use 'està en vista prèvia' instead of 'és una vista prèvia' * fix(i18n/ca): use 'en desenvolupament'; drop article in preview title * fix(i18n/es): use 'en desarrollo' instead of 'en progreso' * fix(i18n/ca): use 'funcions experimentals' instead of 'vista prèvia' * fix(i18n/es): use 'funciones experimentales' instead of 'vista previa' * fix(i18n/ca): use 'funcions experimentals' in preferences.show.preview * fix(i18n/es): use 'funciones experimentales' in preferences.show.preview * fix(i18n/ca): use 'Experimental' pill label instead of 'Vista prèvia' * fix(i18n/es): use 'Experimental' pill label instead of 'Vista previa' --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
1ddd8bd040 |
feat(i18n): complete Catalan translations + extract residual hardcoded strings (#1836)
* feat(i18n): complete Catalan translations + extract residual hardcoded strings
CA coverage
- All view/model/breadcrumb/doorkeeper/mailer locale files for ca: 0 missing
keys (was ~3,400). Translations follow informal "tu" register, sentence case,
domain glossary (Compte/Saldo/Transacció/Posició/Operació/Pressupost/...).
- Catalan pluralization test: ca uses one/other; mirrors
test/lib/polish_pluralization_test.rb.
- 8 LanguageTool-flagged grammar fixes applied (Connexió òrfena, Secret de
l'API, comma-pero, apostrophe elisions, etc).
Hardcoded string extraction (also fixes EN parity)
- UI::Account::Chart#title + chart.html.erb view tabs -> UI.account.chart.*
- UI::Account::BalanceReconciliation labels + tooltips ->
UI.account.balance_reconciliation.{labels,tooltips}.*
- transactions/_transfer_match.html.erb (Auto-matched, A/M, Confirm/Reject
match, Payment/Transfer is confirmed) -> transactions.transfer_match.*
- AccountOrder labels (Name/Balance asc/desc) -> account_order.* keys with
fallback to existing hardcoded labels.
- Depository::SUBTYPES surface in account list -> depositories.subtypes.*.*
- User role badge -> users.roles.* (admin / member / super_admin).
- 110+ country names -> countries.* (config/locales/countries.ca.yml).
Breadcrumb locale fix
- Breadcrumbable was a before_action that ran before Localize's around_action
switched I18n.locale, so default crumbs rendered in EN even when locale=ca.
- Convert to helper_method that defers translation to render-time (when
I18n.locale is already correct). Add all missing breadcrumb keys to ca + en.
- Layouts switched from @breadcrumbs to breadcrumbs helper.
Locale-aware helpers / formatters
- ApplicationHelper#localized_ordinal: ordinalize that respects ca
(1r/2n/3r/4t/Nè). Wired into preferences month_start_day select.
- Family#moniker_label / moniker_label_plural: translate the default "Family"/
"Group" monikers via shared.family_moniker.* with fallback to the family's
custom override.
- Budget#name: use I18n.l for month_year/short/long instead of strftime("%B %Y")
so the budget header date follows the active locale.
Tooling
- script/lt_check_ca.rb: batched LanguageTool checker (premium endpoint when
LT_USERNAME/LT_API_KEY are set, free fallback otherwise), picky mode,
motherTongue=en for false-friend detection.
- lib/tasks/i18n_screenshot.rake: dev-only rake to set user.locale=ca and
role=super_admin on the demo user so the i18n surfaces can be walked.
Out of scope (pre-existing, not introduced here)
- Native browser file input "Choose Files / No file chosen" (browser locale).
- D3.js client-side chart x-axis dates (JS-side Intl.DateTimeFormat needed).
- Sankey/donut labels = seed category names (data, not i18n).
- 2 rails-i18n datetime/errors interpolation warnings inherited from
config/locales/defaults/ca.yml.
* fix(i18n): apply idiomatic Catalan review (3-agent + native review)
Three parallel review agents flagged 203 findings (31 high / 73 medium / 99 low)
across all 111 ca.yml files. This commit applies the high-severity bugs plus a
curated subset of medium-impact fixes.
Grammar / agreement
- provider_sync_summary.health.stale_pending: `(exclòs)` -> `(exclosa/excloses)`
to agree with feminine `transacció(s)`.
- accounts.confirm_unlink.warning_no_sync: added reflexive `es` -
`el compte ja no es sincronitzarà`.
- sophtron_setup_required.heading: `no configurats` -> `sense configurar`
(avoids broken agreement across "ID" masc. + "clau" fem.).
- admin.sso_providers.form.errors_title: split into one/other pluralization
keys (en + ca); singular `ha impedit` was wrong for count > 1.
Brand consistency
- IndexaCapital -> Indexa Capital (37 occurrences across one file).
- Lunchflow -> Lunch Flow in two remaining places.
Anglicisms / domain mistranslations
- kraken_items setup_accounts.instructions: `ompliments d'operacions`
(lit. dental/food fillings) -> `execucions d'operacions`.
- settings kraken_panel.read_only_title: `Sincronització d'intercanvi`
(swap/trade) -> `Sincronització només de lectura amb l'exchange`.
- transactions convert_to_trade.security_custom + security_not_listed_hint:
`cotització` (price quote) -> `ticker` (the EN field IS a ticker symbol).
- loans.form.rate_type: `Tipus d'interès` collided with sibling
interest_rate -> `Modalitat del tipus`.
- brex_items.provider_panel.sandbox_note_html: `L'staging` (broken
contraction) -> `el staging`.
Idiom traps
- coinbase/binance/kraken wait_for_sync: `acabi de sincronitzar` is
ambiguous in CA (`acabar de + inf` reads as "has just done X") ->
`acabi la sincronització`.
- chats.ai_greeting.there: `a tothom` -> `''` (the EN fallback "Hey there"
is singular; literal CA `tothom` is plural and wrong for 1:1 chat).
- transactions.split_parent_row.split_label: `Divideix` (imperative) is
wrong as a status badge -> `Divisió` (noun).
- transactions.keep_both (2 occurrences): infinitive `mantenir ambdues` ->
imperative `mantén-les totes dues` to match the sibling Yes/No buttons.
- rules.clear_ai_cache: `Reinicia` (restart) -> `Buida` (empty/clear),
which matches the success notice (`s'està netejant`).
Moniker gender breakage (cross-file)
%{moniker} is interpolated downcased from family.moniker_label and may
resolve to feminine `família`/`llar` or masculine `grup`. Strings that
hard-code a gendered article ('al teu %{moniker}', 'aquesta %{moniker}',
'aquest/a %{moniker}') broke on at least one branch. Restructured the
affected sentences to drop the gendered determiner:
- account_sharings.show.no_members
- merchants.family_empty / family_title / provider_empty
- registrations.new.join_family_title
- settings.preferences.show.currencies_subtitle / sharing_subtitle
- simplefin_items.select_existing_account.no_accounts_found
- invitations.new.subtitle
- invitation_mailer.invite_email.subject (mailers/) + body (views/)
- snaptrade_items.providers.snaptrade.free_tier_warning
Terminology consistency
- models/account_statement/ca.yml attributes aligned with view-side
forms: `Saldo d'obertura`/`Saldo de tancament` ->
`Saldo inicial`/`Saldo final`; `Suggeriment de...` -> `Pista de...`.
- account_statements.coverage.status.not_expected:
`No s'esperava` -> `No previst` (status label, not past action).
- account_statements.index.empty_unmatched: aligned with the section's
own label `Safata sense aparellar`.
- imports.create.document_provider_not_configured + document_upload_failed:
`arxiu vectorial` -> `magatzem vectorial` (correct TermCat term).
- coinstats_items blockchain gender: `els blockchains` / `un blockchain` ->
`les blockchains` / `una blockchain` (feminine per TermCat).
- accounts.account.remove_default: `Treu el predeterminat` ->
`Treu com a predeterminat` (pairs with sibling `Estableix com a
predeterminat`).
- accounts.tax_treatments.tax_deferred: `Diferit fiscalment` (lit. calque)
-> `Tributació diferida` (standard CA tax-accounting term).
- settings.payments.show.currently_on_plan: `Actualment al` ->
`Actualment al pla:` (was a fragment).
Out of scope (review flagged, not applied here)
- LOW-severity stylistic preferences (Veure vs Mostra, etc).
- `models/category/ca.yml` default category names — seeded at family
creation, not via I18n at runtime, so changes wouldn't affect existing
families.
- `models/period/ca.yml` short labels mixing EN (MTD/YTD) and CA (STD/MA)
— needs a one-convention decision separately.
* fix(i18n,ca): drop gendered article in period_activity + tighten cash-flow terms
- pages.dashboard.investment_summary.period_activity: 'Activitat del
%{period}' contracted 'del' = 'de el' (masc.sg.). %{period} resolves
to mixed forms ('Setmana en curs' fem, 'Últims 30 dies' pl., 'Any en
curs' apostrophe), so hard-coded 'del' was wrong on most labels.
Replaced with 'Activitat — %{period}' (em-dash) to skip the
contraction entirely.
- pages.dashboard.outflows_donut.title / total_outflows: switched from
bare 'Sortides' / 'Total de sortides' to 'Sortides de caixa' /
'Total de sortides de caixa' to match TermCat's precise term
('sortida de caixa' = cash outflow).
* fix(i18n,ca): rephrase transfer source/destination amount labels
'Import d'origen' / 'Import de destinació' were literal calques of
'Source amount' / 'Destination amount'. In a multi-currency transfer
form (sender/receiver in different currencies) the natural CA pair is
'Import enviat' / 'Import rebut'.
* fix(i18n,ca): 'Dades en brut' -> 'Dades sense processar'
The literal calque of 'Raw data' read as too technical for personal-
finance UI. 'Dades sense processar' is the more natural Catalan
equivalent for raw/unprocessed data files.
* fix(i18n): localize Import col_sep label + separator options
The CSV upload form rendered 'Col sep' (the auto-humanized attribute
name) plus hardcoded English 'Comma (,)' / 'Semicolon (;)' options
from Import::SEPARATORS.
- activerecord.attributes.import.col_sep added (en + ca: 'Column
separator' / 'Separador de columnes').
- Import.separator_options class method returns translated tuples;
view switched from Import::SEPARATORS to Import.separator_options.
- activerecord.attributes.import.col_seps.{comma,semicolon} added so
the option labels follow the active locale.
* fix(i18n,ca): drop moniker apposition in sharing/currencies section titles
- sharing_title 'Compartició de %{moniker}' rendered as 'Compartició
de Família' (a noun-noun apposition that's odd in CA) -> 'Compartició
de comptes'.
- sharing_subtitle replaced '%{moniker}' with 'entre els membres' so
the sentence reads naturally and doesn't depend on moniker gender.
- currencies_title 'Divises de %{moniker}' had the same apposition
-> 'Divises'. Subtitle no longer references moniker either.
* fix(i18n,ca): keep 'Self Hosting' untranslated
Reverted 'Autoallotjament' / 'autoallotjada' / 'autoallotjats' usages
to the original English 'Self Hosting' (sidebar label, breadcrumbs,
hostings page title, chat assistant settings hint, redis configuration
subheading, LLM usages cost-estimates description).
The brand-style term reads more naturally in EN for technical users
configuring their own deployment.
* fix(i18n,ca): lowercase 'self hosting' (sentence case in labels)
* fix(i18n): extract budget_categories stepper + allocation_progress strings
Hardcoded English strings on the budget category editor:
- 'Setup' / 'Categories' stepper labels in budgets/_budget_nav.html.erb
- 'X% set' / '> 100% set' / 'left to allocate' / 'Budget exceeded by ...'
in budget_categories/_allocation_progress.erb
- '/m avg' caption + 'Shared' placeholder + 'Leave empty to share
parent's budget' tooltip in budget_categories/_budget_category_form
and _uncategorized_budget_category_form
Extracted to:
- budgets.budget_nav.{setup,categories}
- budget_categories.allocation_progress.{percent_set,over_set,left_to_allocate,budget_exceeded_html}
- budget_categories.budget_category_form.{monthly_average,shared_placeholder,shared_title}
CA translations added; EN keys mirror the prior literals.
* chore(i18n): drop translation tooling from PR
These were dev-only helpers used during the Catalan translation pass:
- script/lt_check_ca.rb: LanguageTool API checker (premium/free
endpoint, picky mode, batching). Useful for ongoing locale QA but
shouldn't ship in this feature PR.
- lib/tasks/i18n_screenshot.rake: rake task that flips user.locale and
role on the demo user for walking the i18n surfaces locally.
Both stay available locally; pulled out of the PR scope.
* fix(i18n): apply PR review feedback (CodeRabbit + Codex)
- balance_reconciliation crypto_items: use :end_balance_crypto tooltip
(was :end_balance_investment). Added new UI.account.balance_reconciliation.tooltips.end_balance_crypto key in en + ca.
- doorkeeper.ca.yml confidentiality.no: was YAML boolean false, now string 'No'.
- views/categories: 'Poor contrast, choose darker color or' continued with hardcoded 'auto-adjust.' button text; extracted to categories.form.auto_adjust key (en + ca).
- imports.create.document_upload_failed: 'a l'magatzem' was broken
contraction -> 'al magatzem'.
- invitation_mailer body + mailer subject: 'unir-se' -> 'unir-te' (was
3rd person, should be 2nd to match the rest of the copy).
- 7 strings across mercury_items / sophtron_items / simplefin_items /
lunchflow_items / brex_items / indexa_capital_items / other_assets:
'se sincronitzaran' -> 'es sincronitzaran', 'se segueixen' ->
'es segueixen' (correct reflexive pronoun before consonants).
- settings.providers.status: key was 'false' (YAML-coerced), now 'off'
to match settings/en.yml status.off used in view lookups.
- sophtron_items.sophtron_setup_required.message: stripped trailing
blank line from the quoted scalar.
- settings/profiles/show.html.erb: switched 'family_moniker ==
"Group"' branch checks to 'Current.family&.moniker == "Group"'.
After Family#moniker_label started returning translated values,
callers using the display label for branching would render the
household copy for group families in ca. Compare the stored sentinel
instead.
- Did not apply CodeRabbit's webauthn 'eliminada' -> 'desada' suggestion:
the key is wired to the destroy action (verified at
settings/webauthn_credentials_controller.rb:55), so 'eliminada' is
correct.
|
||
|
|
7411db5689 |
feat(i18n): add Hungarian translations for strings extracted in #1806 (#1817)
* add missing Hungarian translations for newly extracted strings Replace hard-coded UI strings with I18n lookups across controllers, models and views (breadcrumbs, dashboard, reports, settings, transactions, balance sheet, MFA status). Update models to use translations for category defaults, account/display names, classification group and period labels; remove a few hardcoded display_name methods. Add and update numerous locale files (English and extensive Hungarian translations, plus model/view/doorkeeper entries) to provide the required keys. These changes centralize copy for localization and prepare the app for Hungarian/English UI text. * Pluralize account type labels; tidy Crypto model Update English locale account type labels to use plural forms for consistency (Investment(s), Properties, Vehicles, Other Assets, Credit Cards, Loans, Other Liabilities). Also remove an extra blank line in app/models/crypto.rb to tidy up formatting. * Back to singular * fix(i18n): separate singular and group account labels * Update _accountable_group.html.erb * Use I18n plural names for account types Change Accountable#display_name to look up pluralized account type names via I18n (accounts.types_plural.<underscored_class>) with a fallback to the legacy display logic. Add legacy_display_name helper to preserve previous behavior (singular for Depository and Crypto, pluralized otherwise). Add corresponding types_plural entries in English and Hungarian locale files for various account types. --------- Co-authored-by: Juan José Mata <jjmata@jjmata.com> Co-authored-by: sure-admin <sure-admin@splashblot.com> |
||
|
|
5249842c76 |
feat: beta features toggle + Beta pill primitive (#1829)
* feat: beta features toggle + Beta pill primitive Adds the infrastructure for self-service beta opt-in. No call sites yet: this PR is meant to land first so feature PRs (Goals, etc.) can ship behind the gate incrementally. User opts in via a single toggle at the bottom of Settings → Preferences. The flag persists in the existing `users.preferences` JSONB column under `beta_features_enabled` — same shape as `dashboard_two_column` and `show_split_grouped`, so no migration is needed. Controllers gate a beta feature by adding `before_action :require_beta_features!` from the new `BetaGateable` concern (included in ApplicationController). Views use the `beta_features_enabled?` helper to hide / show nav items, banners, etc. Logged-out callers always return false. Ships `DS::BetaPill`, a small inline marker for tagging features as Beta / Canary in nav, headers, and lists. Five tones (violet by default, indigo, fuchsia, amber, gray) map to existing Sure color tokens — no raw hex. Three styles (soft / filled / outline) and two sizes (sm / md) cover the surfaces in the design handoff. The `dot_only:` mode renders just the colored dot for use on a collapsed sidebar. * review: rename to DS::Pill, fix CR/Codex nits, add tests CodeRabbit + Codex review feedback: - Rename DS::BetaPill → DS::Pill. The component was already generic in shape (tones, styles, sizes); the name was misleading scope. "Beta" becomes the default label (still i18n-driven). Goals' StatusPill can later refactor onto this primitive without a third pill. - Localize the default pill label via i18n (`ds.pill.default_label`) instead of hard-coding English. - Add role="img" to the dot-only span so the aria-label is consistently exposed to assistive tech. - Wrap the Preferences toggle row in <label for="…"> so the title and description become an honest click target for the toggle (matches the cursor-pointer affordance). - Drop arbitrary Tailwind values (py-[3px], gap-[5px], tracking-[…]) in favor of scale tokens. text-[10/11px] stays because the pill is intentionally sub-12px (Sure's smallest scale token is text-xs / 12px) to read as a marker, not a label. - Add User#beta_features_enabled? predicate tests covering default-off, explicit-true, and non-boolean truthy values. Won't fix: - Palette refs (`--color-violet-*` etc.). Sure has no semantic Beta/ Canary tokens; introducing them in this PR would be a design-system change beyond the scope. The component centralizes palette use in one `palette` method, matching the existing pattern in Goals::StatusPillComponent. * review: consistent title fallback in full-pill branch * docs: how to gate a feature behind the beta toggle * docs: unwrap doc lines to match existing style * chore(preview): run Cloudflare PR previews on basic instances (#1831) * fix(preview): use Rails health endpoint for container ping (#1823) * fix(preview): use Rails health endpoint for container ping * fix(preview): point container ping to localhost/up --------- Co-authored-by: Sure Admin (bot) <sure-admin@splashblot.com> |
||
|
|
4fd460d551 |
Add Actual Budget CSV import flow (#1830)
* Add Actual Budget CSV import flow * Address Actual import review feedback |
||
|
|
70fc52769d |
Add super_admin debug event log (#1816)
* Add super-admin debug event log * Address debug log review feedback * Whitelist debug filter params * Make debug log retention configurable |
||
|
|
0c126b1674 |
feat(i18n): extract hardcoded English strings to locale files (#1806)
* Extract hardcoded strings to i18n
Replace numerous hardcoded English strings with I18n lookups (t / I18n.t) across controllers, views, helpers, and components, and convert model validation error messages to symbol keys. Added multiple locale files under config/locales for models and views. This centralizes user-facing notices/alerts, UI text, import/validation messages, and prepares the app for localization and easier translation maintenance.
* Update en.yml
* Update preview-cleanup.yml
* Revert "Update preview-cleanup.yml"
This reverts commit
|
||
|
|
04549d80bf |
fix(rules): count blocked rule transactions (#1782)
* Add blocked count to rule run summary * test(rules): cover rule run blocked counts * fix(rules): derive blocked count from modified rows Blocked rule transactions are the processed rows that were not modified. This keeps the displayed queued / processed / modified / blocked summary aligned when a run has already processed all matching rows but some were skipped by enrichment locks. * fix(rules): count processed rows for rule jobs Synchronous rule actions return the number of rows they modified, but rule-run processed counts should represent the number of matched transactions the job attempted to process. Using queued matches for processed preserves the distinction between processed and modified rows, which lets locked manual edits appear as blocked instead of making processed collapse to modified. This changes RuleJob counter semantics, so it was committed separately from the derived blocked-count display change. |
||
|
|
0ad1e59165 |
fix(a11y): add skip-link and aria-current="page" to application layout (#1781)
* fix(a11y): add skip-link and aria-current="page" to application layout * test(a11y): cover application layout skip-link and #main anchor * fix(a11y): extend skip-link and #main anchor to settings layout |