* chore(deps): upgrade Rails 7.2 → 8.1
Rails 7.2 reaches end of life on 2026-08-09. Bump the framework to the
current 8.1.x line.
- Gemfile: rails "~> 8.0" (resolves 8.1.3); bundle update rails pulls the
Rails 8 framework gems plus the bumps it requires — ViewComponent
3.23 → 4.x (Rails 8 support), rails-i18n 7 → 8, rswag, and transitive deps.
- app/models/transfer.rb: make Transfer#date nil-safe
(inflow_transaction&.entry&.date). Rails 8's date_field evaluates the
field default on a new/unpersisted Transfer (the new-transfer form), where
the association is nil; without this, TransfersController#new raises
"undefined method 'entry' for nil". Matches the &. pattern already used in
Transfer#sync_account_later.
Framework behavioral defaults are unchanged (config.load_defaults stays as-is).
Validated on Rails 8.1.3: zeitwerk:check passes, full suite green
(4904 runs, 0 failures, 0 errors), rubocop and brakeman clean.
* fix(rails8): style textarea + deterministic property edit system test
The Rails 8 gem bump kept config.load_defaults at 7.2, but Rails 8 renamed
two ActionView::Helpers::FormBuilder field helpers regardless of defaults:
:text_area → :textarea and :check_box → :checkbox. StyledFormBuilder builds
its styled helpers from `field_helpers`, so `form.text_area` (e.g. the
account "Notes" field) silently fell through to the unstyled base helper and
rendered without a label — failing 8 system tests with
`Unable to find field "Notes"`.
- app/helpers/styled_form_builder.rb: exclude both spellings of the
non-text helpers (:check_box and :checkbox) and alias the legacy
`text_area` to the Rails 8 `textarea` so existing call sites stay styled.
Harmless on Rails 7.2 (old names present instead).
- test/system/property_test.rb: open the property edit dialog via the
account menu with a retry. The account page issues a Turbo morph refresh
shortly after load (turbo_refreshes_with :morph + a family-stream
broadcast); opening the modal while that refresh is in flight let the
morph re-render the page and wipe the just-loaded #modal turbo-frame.
Rails 8 timing made the race deterministic. Retrying once the refresh has
settled makes the test stable (confirmed via Turbo frame-load vs
full-page morph event traces; 3x green in isolation).
- config/brakeman.ignore: the added comment block shifted the pre-existing
(already-ignored, Weak) class_eval Dangerous Eval warning from line 5 -> 10,
changing its fingerprint. Re-point the existing suppression to the new
fingerprint/line so scan_ruby stays green.
Validated on Rails 8.1.3: full system suite green
(92 runs, 355 assertions, 0 failures, 0 errors), rubocop clean,
brakeman 0 warnings, CodeRabbit no findings.
* chore(deps): pin rails to the 8.1 minor line (~> 8.1.0)
Tighten the constraint from `~> 8.0` to `~> 8.1.0` (>= 8.1.0, < 8.2) so a
future `bundle update rails` tracks the 8.1.x line rather than silently
jumping to 8.2 when it ships. Matches the upgrade plan's stated intent
(target 8.1.x for the EOL runway) and a review note on #2301.
No resolved-version changes: bundle install keeps rails at 8.1.3 and every
other locked gem unchanged — only the Gemfile.lock DEPENDENCIES constraint
line moves. zeitwerk:check still passes; the already-green unit/system
suites ran on this exact resolved tree.
* chore(rails8): adopt Rails 8.1 framework defaults (config.load_defaults 8.1)
The gem bump above kept config.load_defaults at 7.2 so the change set could be
reasoned about in stages; this finalizes the upgrade by adopting the modern
framework defaults now that the suite is green on Rails 8.1.
Rails 8.0 added no new framework defaults (there is no new_framework_defaults_8_0
template), so 7.2 -> 8.1 is the single meaningful step. No incremental
new_framework_defaults_8_1.rb opt-in file is needed: the full suites pass with all
8.1 defaults enabled at once.
The 8.1 defaults this turns on include action_on_path_relative_redirect=:raise
(open-redirect hardening), raise_on_missing_required_finder_order_columns,
escape_json_responses=false / escape_js_separators_in_json=false (JSON perf), and
Ruby-parser template-dependency tracking.
Validated with no application code changes: bin/rails test 4904/0/0,
bin/rails test:system 92/0/0, rubocop + brakeman clean.
* chore(ci): restore brakeman CheckEOLRails now that the app is on Rails 8.1
config/brakeman.yml existed only to skip brakeman's CheckEOLRails. That check
fires on the calendar (it warns 60 days before a framework's EOL and escalates
as the date nears), so Rails 7.2's 2026-08-09 EOL turned `bin/brakeman` red
(exit 3) on every branch and on main regardless of the diff. The skip carried a
TODO to remove it once Sure upgraded off 7.2.
This PR puts the app on Rails 8.1 (EOL well in the future), so the skip is
obsolete; remove the file (its sole content was the skip) in the same change that
makes it unnecessary -- no stale-config window. brakeman auto-loads the file when
present and falls back to defaults when absent, and nothing references it
explicitly. CheckEOLRuby was already enabled and is unchanged; config/brakeman.ignore
is untouched.
Validated on Rails 8.1: bin/brakeman runs EOLRails + EOLRuby, 0 warnings,
0 errors, exit 0.
* 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
624e9794 doesn't cover it. Pass `type: :submit` explicitly so the
profile form submits again under the default-type-button policy.
3. base.css: replace raw `outline-gray-900` / `outline-white` with the
established alpha-ring focus pattern
(focus-visible:ring-alpha-black-300 + theme-dark:ring-alpha-white-300)
already used by app/components/settings/provider_card.html.erb and
sure-design-system/components.css. Keeps a11y focus ring while using
DS tokens.
* fix(review): add type: :submit to DS::Button submitters inside forms
CI test_system on #1840 surfaced 6 failures (confirm-dialog close,
property create/edit, transaction filter apply) caused by the same
gap that db563f3d started addressing: the default-type-button policy
on DS::Button means every \`render DS::Button.new(...)\` inside a
\`<form>\` (or \`styled_form_with\`) that relies on the HTML default to
submit is now an inert \`type="button"\`.
Audited every \`render DS::Button.new(\` callsite repo-wide for the
combination (no \`type:\`, no \`href:\`, inside a form context) and
pinned \`type: :submit\` explicitly on the 12 forms that need it:
- layouts/shared/_confirm_dialog.html.erb: Confirm button inside the
global \`<form method=\"dialog\">\` — fixes
test_should_allow_revoking_API_key_with_confirmation.
- properties/{new,edit,balances}.html.erb: Save/Next submitter inside
\`styled_form_with\` — fixes test_can_create_property_account,
test_can_persist_property_subtype.
- transactions/searches/_menu.html.erb: Apply inside the filter form —
fixes test_can_filter_uncategorized_transactions,
test_all_filters_work_and_empty_state_shows_if_no_match,
test_can_open_filters_and_apply_one_or_more.
- transactions/bulk_updates/new.html.erb: Save in bulk-edit drawer.
- account_sharings/show.html.erb: Save in account-sharing form.
- category/deletions/new.html.erb, tag/deletions/new.html.erb:
destructive + safe submit buttons in deletion dialog forms.
- family_merchants/merge.html.erb: Submit in merge form.
- subscriptions/upgrade.html.erb: contribute_and_support_sure submit.
- rules/_category_rule_cta.html.erb: Dismiss inside the
rule_prompts_disabled form.
Cancel/close DS::Button instances inside these same forms intentionally
keep the \`type=button\` default since they drive JS-only actions
(\`DS--dialog#close\`, \`DS--menu#close\`).
* fix(review): add type: :submit to 4 remaining form-context DS::Button callers
Second sweep for the same default-type-button regression that 24c517eb
fixed for 12 callsites. The latest CI run on this branch narrowed the
failures from 6 to 2 (the property wizard's Address step still failed
because that view was not in the first sweep). Audited via a wider
4000-char form-context window:
- app/views/properties/address.html.erb: Save inside
styled_form_with — fixes the remaining
test_can_create_property_account + test_can_persist_property_subtype
by letting Step 3 of the property wizard complete.
- app/views/onboardings/goals.html.erb: Submit inside form_with so
the onboarding goals step submits.
- app/views/account_sharings/show.html.erb (owner-side form): Save
button for the family-share permissions form (the non-owner Save
was already fixed in 24c517eb).
- app/views/transactions/_attachments.html.erb: Upload inside
styled_form_with — kept the JS-driven hook (attachment_upload_target)
but explicit type:submit covers the no-JS fallback.
* fix(review): pin type=submit on the Save currencies button
Codex P1 (third pass) caught one more in-form DS::Button I missed in
the earlier sweeps: \`app/views/settings/preferences/show.html.erb:185\`
renders the Save currencies submit deep inside a long
\`styled_form_with\` block. The form-context scan I used had a finite
look-back window which missed it because the matching
\`styled_form_with\` opener sits ~80 lines / 4k+ characters above the
button. Switched to a whole-file scan to confirm no further callsite
remains.
* feat(select): improve merchant dropdown behavior and placement controls
- add configurable menu_placement strategy to DS::Select (auto/down/up) with safe normalization
forward menu_placement through StyledFormBuilder#collection_select
- force Merchant dropdown to open downward in transaction create and editor forms
- fix select option/search text contrast by applying text-primary in DS select menu
- prevent form jump on open by scrolling only inside dropdown content instead of using scrollIntoView
- clamp internal dropdown scroll to valid bounds for stability
- refactor select controller placement logic for readability (placementMode, clamp) without changing behavior
* set menu_placement=auto for metchant selector
* feat: add new UI component to display dropdown select with filter
* feat: use new dropdown componet for category selection in transactions
* feat: improve dropdown controller
* feat: Add checkbox indicator to highlight selected element in list
* feat: add possibility to define dropdown without search
* feat: initial implementation of variants
* feat: Add default color for dropdown menu
* feat: add "icon" variant for dropdown
* refactor: component + controller refactoring
* refactor: view + component
* fix: adjust min width in selection for mobile
* feat: refactor collection_select method to use new filter dropdown component
* fix: compute fixed position for dropdown
* feat: controller improvements
* lint issues
* feat: add dot color if no icon is available
* refactor: controller refactor + update naming for variant from icon to logo
* fix: set width to 100% for select dropdown
* feat: add variant to collection_select in new transaction form
* fix: typo in placeholder value
* fix: add back include_blank property
* refactor: rename component from FilterDropdown to Select
* fix: translate placeholder and keep value_method and text_method
* fix: remove duplicate variable assignment
* fix: translate placeholder
* fix: verify color format
* fix: use right autocomplete value
* fix: selection issue + controller adjustments
* fix: move calls to startAutoUpdate and stopAutoUpdate
* Update app/javascript/controllers/select_controller.js
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com>
* fix: add aria-labels
* fix: pass html_options to DS::Select
* fix: unnecessary closing tag
* fix: use offsetvalue for position checks
* fix: use right classes for dropdown transitions
* include options[:prompt] in placeholder init
* fix: remove unused locale key
* fix: Emit a native change event after updating the input value.
* fix: Guard against negative maxHeight in constrained layouts.
* fix: Update test
* fix: lint issues
* Update test/system/transfers_test.rb
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com>
* Update test/system/transfers_test.rb
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com>
* refactor: move CSS class for button select form in maybe-design-system.css
---------
Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Add lookbook + viewcomponent, organize design system file
* Build menu component
* Button updates
* More button fixes
* Replace all menus with new ViewComponent
* Checkpoint: fix tests, all buttons and menus converted
* Split into Link and Button components for clarity
* Button cleanup
* Simplify custom confirmation configuration in views
* Finalize button, link component API
* Add toggle field to custom form builder + Component
* Basic tabs component
* Custom tabs, convert all menu / tab instances in app
* Gem updates
* Centralized icon helper
* Update all icon usage to central helper
* Lint fixes
* Centralize all disclosure instances
* Dialog replacements
* Consolidation of all dialog styles
* Test fixes
* Fix app layout issues, move to component with slots
* Layout simplification
* Flakey test fix
* Fix dashboard mobile issues
* Finalize homepage
* Lint fixes
* Fix shadows and borders in dark mode
* Fix tests
* Remove stale class
* Fix filled icon logic
* Move transparent? to public interface
* Remove stale 1.0 import logic and model
* Fresh start
* Checkpoint before removing nav
* First working prototype
* Add trade, account, and mint import flows
* Basic working version with tests
* System tests for each import type
* Clean up mappings flow
* Clean up PR, refactor stale code, tests
* Add back row validations
* Row validations
* Fix import job test
* Fix import navigation
* Fix mint import configuration form
* Currency preset for new accounts
* Make forms more composable, opt-in to form builder
* Remove unused method
* Simpler money input controls
* Add in new form styling to imports
* Lint fixes
* Small tweak of multi select styles