* feat(debug): add expandable view to the debug event log
The /settings/debug table packs seven columns of long-form diagnostics
into one row, so messages, context IDs, and metadata are all cramped and
hard to read.
Borrows the expand pattern from the dashboard cashflow chart (#739):
hovering a log row reveals a DS::Button icon trigger (always visible
below `lg`, where there is no hover state, and on keyboard focus) that
opens the entry in a roomy DS::Dialog. The expanded view lays the entry
out vertically — level/category as DS::Pill badges, the full message,
source, each context ID labelled, and pretty-printed metadata in a
scrollable block.
- Extract the row into a `_log_entry` partial now that it carries the
trigger and its dialog.
- Add a generic `expandable` Stimulus controller that opens the <dialog>
inside its scope, since the trigger sits outside the DS--dialog
controller's scope.
- Add `Settings::DebugsHelper` for the level-to-pill-tone mapping and the
context field list, keeping the logic out of the template.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRNRykqxBvgWwA2Wm8KiXH
* refactor(debug): expand the whole log table, not individual rows
The expand affordance belongs on the table card, matching the dashboard
cashflow chart it is modelled on: one trigger in the card's header strip
reopens the entire log in a near full-width dialog, where all seven
columns finally have room.
- Extract the table into a `_log_table` partial so the inline card and
the expanded dialog render the same markup, and give its header
`sticky top-0` — inert inline, useful once the expanded copy scrolls.
- Drop the per-row trigger, its detail dialog, and the `Settings::DebugsHelper`
and locale keys that only existed to support them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRNRykqxBvgWwA2Wm8KiXH
* fix(debug): reveal expand button on coarse pointers, lift width to DS
Addresses review feedback on the expandable debug log table.
- The expand trigger was hidden by `lg:opacity-0` and only revealed by
hover, so a touch-only device wide enough to hit `lg` — a large tablet —
had no way to discover it. Viewport width is the wrong proxy for hover
capability; switch to the shape the dashboard insights feed already
uses: hidden by default, revealed by hover, focus, or `pointer-coarse`.
- The `!w-[96vw] max-w-[1650px]` expanded-dialog shape was hand-rolled at
the callsite in two places. Lift it into `DS::Dialog::WIDTHS[:expanded]`
and use it from both the debug log and the dashboard cashflow chart, so
the arbitrary values live in the design system rather than in views. The
emitted class string is unchanged in both cases.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRNRykqxBvgWwA2Wm8KiXH
* fix(ds): stop the expanded dialog overflowing narrow viewports
`!w-[96vw]` applied at every breakpoint, but `dialog_inner_classes` only
drops its `mx-3` gutter at `lg`. Below that the panel is 96vw + 24px of
margins inside a dialog box that is narrower than the viewport — `vw`
counts the scrollbar, the dialog's percentage-based box does not — so the
panel is clipped on both sides. Flex-shrink cannot absorb it either, once
nowrap content (the debug log's timestamp and context cells) raises the
panel's min-content width.
Scope the 96vw to `lg` and up, where the gutter is gone. Below `lg` the
base `w-full` + `mx-3` already fits, which is what every other dialog
width does.
Measured in Chromium against Tailwind 4.1.8 output, panel clipped per side:
viewport 320 375 390 768 1024 1400
!w-[96vw] 12.6 11.5 11.2 0 0 0
lg:!w-[96vw] 0 0 0 0 0 0
Widths at 768/1024/1400 are unchanged (706/978/1344px).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRNRykqxBvgWwA2Wm8KiXH
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add a user preference under Settings → Appearance that prevents modals
from closing when clicking outside them. Useful to avoid accidentally
losing unsaved form data.
- Add `disable_modal_click_outside?` helper to User model (JSONB prefs)
- DS::Dialog reads the user preference as default when not explicitly set
(existing callers passing disable_click_outside: true/false are unaffected)
- Wire up save in AppearancesController
- Add toggle in the Modals section of the Appearance settings page
- Add i18n strings
* fix(i18n): move modal keys to appearances.show namespace in 7 locales
The modal translation keys (modals_title, modals_subtitle,
disable_modal_click_outside_title, disable_modal_click_outside_description)
were under settings.preferences.show but the view calls t(".modals_title")
from settings/appearances/show.html.erb. Moved them to
settings.appearances.show in de, es, nb, nl, ro, tr, and zh-TW.
* refactor(ds): decouple Dialog from Current.user via defaults_provider
---------
Co-authored-by: neko <neko@nixos>
The dialog close button rendered as a :md icon button (44x44px with a
20px glyph) — noticeably larger than the dialog's own action buttons
(36px tall) and visually heavy next to the title. Pass size: :sm so the
close control is 32x32px with a 16px glyph, matching the action row's
weight. 32px still clears the WCAG 2.5.8 (AA) 24px minimum target.
* fix(design-system): DS::Dialog a11y — role, aria-modal, aria-labelledby, heading_level
Closes#1740. The savings-goals audit captured the dialog rendering
without `role`, `aria-modal`, or `aria-labelledby` — AT users
landing focus inside the dialog hear no title and no modal-mode
hint. Affects every modal/drawer surface in the app (transfer
matches, valuations, trades, imports, settings, etc. — 30+ views).
Fixes:
1. `role="dialog"` + `aria-modal="true"` on the `<dialog>` element.
Native `<dialog>` already maps to these implicitly in modern
browsers, but Safari and pre-2024 mappings benefit from the
explicit role.
2. `aria-labelledby` wired to a stable `dialog-title-<8-char hex>`
id minted in initialize. The header slot's `<h*>` carries the
matching id; AT now announces the title on focus-in. If the
caller passes `custom_header: true` (no title), the
`aria-labelledby` reference resolves to nothing and AT
gracefully falls back to the first focusable.
3. New `heading_level:` kwarg (default `2`). Lets callers nest
dialogs inside surfaces that already have an `<h2>` heading
without breaking outline order. The existing `<h2>` baseline
stays as the default.
API is additive; existing 30+ DS::Dialog callsites work without
modification.
Out of scope (own issues):
- Drawer modal-vs-non-modal split (`<dialog>` is currently always
opened via `showModal()`). Browser behavior is correct for both
variants today; non-modal drawer is a separate UX call.
- Reduced-motion audit — no CSS transitions on `dialog` open/close.
- Explicit focus-on-open (title vs first input) — browser-native
`showModal()` already focuses the first focusable; caller can
override with `autofocus`. Not changing the default here.
- `en.common.close` missing translation — separate bug, filed.
* fix(review): gate aria-labelledby + validate heading_level
Only emit aria-labelledby when the header slot rendered an auto-title
so the id reference never dangles (custom_header: true and body-only
dialogs like the global confirm dialog no longer expose a broken
label). Validate heading_level is an Integer 1..6 in the initializer
to prevent invalid <h0>/<h7> markup. Update stale comment that
referenced tag.public_send instead of content_tag.
* fix(ds-dialog): always emit aria-labelledby (slot lambda is lazy)
The previous fix gated `aria-labelledby` on `@has_auto_title`, set
inside the `renders_one :header` slot lambda. ViewComponent v3
evaluates slot lambdas lazily at slot-render time (after the parent
template's `tag.dialog` opening attributes are computed), so the
flag was always `false` when the `aria-labelledby` attribute was
read.
Verified end-to-end via Playwright on `/design-system/preview/dialog/{modal,drawer}`:
the rendered `<dialog>` is missing `aria-labelledby` even when
`with_header(title: ...)` is set, despite the matching `<h2 id="dialog-title-...">`
being present in the DOM. AT therefore announces "dialog" with no
title — the exact regression the PR set out to fix on slot-driven
callers (which is every dialog in the app).
Always emitting `aria-labelledby="dialog-title-<hex>"` is safe per
the WAI-ARIA spec: a dangling reference (e.g. `custom_header: true`
or body-only dialogs) is silently ignored, and callers can override
via `**opts` (last-wins). This matches the intent stated in the PR
body of #1740.
- Drop now-dead `@has_auto_title` ivar + `has_auto_title?` predicate.
- Update template comment to explain the slot-lambda timing trap.
DS::Dialog#close_button called I18n.t("common.close") but no
`common.close` key exists in any locale file, so every modal rendered
the literal string "Translation missing: en.common.close" as both the
`title` and `aria-label` of the X close button — visible to screen
readers and as a hover tooltip.
Switch to `ds.dialog.close` to mirror the existing `ds.alert.*`
namespace under config/locales/views/components/*.yml, and add the
English string. Other locales fall back to English (fallbacks=true in
config/application.rb) until translated.
Closes#1763.
Co-authored-by: plind-junior <plind-junior@users.noreply.github.com>
* feat: Add responsive dialog behavior for transaction modals
Add responsive option to DS::Dialog component that switches between:
- Mobile (< 1024px): Modal style (centered) with inline close button
- Desktop (≥ 1024px): Drawer style (right side panel) with header close button
Update transaction, transfer, holding, trade, and valuation views to use
responsive behavior, maintaining mobile experience while reverting desktop
to drawer style like budget categories.
Changes:
- app/components/DS/dialog.rb: Add responsive parameter and helper methods
- app/components/DS/dialog.html.erb: Apply responsive styling
- app/views/*/show.html.erb: Add responsive: true and hide close icons on mobile
* fix: Enhance close button accessibility in dialog components
* fix: Refactor dialog component to improve close button handling and accessibility
* feat: implement expandable view for cashflow sankey chart
* refactor: migrate cashflow dialog sizing to tailwind utilities
* refactor: declarative draggable restore on cashflow dialog close
* refactor: localized title and use Tailwind utilities
* refactor: update dialog interaction especially on mobile
* refactor: add global expand text to localization
* fix: restore draggable immediately after dialog close
* Whitespace noise
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* Introduce SnapTrade integration with models, migrations, views, and activity processing logic.
* Refactor SnapTrade activities processing: improve activity fetching flow, handle pending states, and update UI elements for enhanced user feedback.
* Update Brakeman ignore file to include intentional redirect for SnapTrade OAuth portal.
* Refactor SnapTrade models, views, and processing logic: add currency extraction helper, improve pending state handling, optimize migration checks, and enhance user feedback in UI.
* Remove encryption for SnapTrade `snaptrade_user_id`, as it is an identifier, not a secret.
* Introduce `SnaptradeConnectionCleanupJob` to asynchronously handle SnapTrade connection cleanup and improve i18n for SnapTrade item status messages.
* Update SnapTrade encryption: make `snaptrade_user_secret` non-deterministic to enhance security.
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>