mirror of
https://github.com/we-promise/sure.git
synced 2026-05-24 21:14:56 +00:00
5520bacbb87e565ffa2e434862df8f6e7ea7781d
2658 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5520bacbb8 |
fix(i18n): standardize product name interpolation in import mapping descriptions (#1956)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> |
||
|
|
c93193cfbc |
fix(locale): Handle blank locale submission gracefully (#1876)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> |
||
|
|
c7c63a50a7 | Add PR workflow for not-gittensor labeling (#1957) | ||
|
|
0988e2d9d6 |
perf: use jemalloc as the default allocator (#1910)
* feat(docker): add jemalloc to reduce memory fragmentation Install libjemalloc2 in the base image and preload it via LD_PRELOAD in docker-entrypoint when available. Reduces RSS growth from glibc's default allocator fragmentation under Rails workloads. * feat(docker): add DISABLE_JEMALLOC env var + preserve existing LD_PRELOAD * feat(docker): add jemalloc status logging to entrypoint * refactor(docker): simplify jemalloc logging to warn-only when disabled/missing |
||
|
|
ca895416a4 |
chore(helm): bump pipelock to 2.5.0 and surface 2.5 config (#1913)
* chore(helm): bump pipelock to 2.5.0 and surface 2.5 config Bumps pipelock.image.tag from 2.2.0 to 2.5.0 and exposes the most relevant 2.5 features as structured Helm values: - pipelock.requestBodyScanning: scan outbound bodies and sensitive headers for prompt-injection and DLP payloads. Disabled by default; roll out with action=warn before flipping to block. - pipelock.healthWatchdog: structured config for the wedge-detection watchdog with an exposeSubsystems toggle for /health detail. - pipelock.mcpToolPolicy.rules: structured values for rendering mcp_tool_policy.rules including redirect-profile references. Also fixes a latent config-validation regression: pipelock 2.x rejects an enabled mcp_tool_policy with no rules, but the chart previously defaulted to enabled=true with an empty rules list, which hard-fails 'pipelock check'. The default is now enabled=false; operators must explicitly enable and provide at least one rule. Refreshes README, CHANGELOG, docs/hosting/pipelock.md, docs/hosting/ai.md, compose example pin comment, and pipelock.example.yaml to call out 2.5 highlights (Audit Packet v0 verifiers, SPIFFE-strict envelopes, scanner attribution on MCP block receipts, pipelock doctor). Also fixes a stale docs/hosting/mcp.md reference to the removed compose.example.pipelock.yml. * chore(helm): fail helm template when mcp_tool_policy enabled with no rules Adds a guard in asserts.tpl so an operator who sets pipelock.mcpToolPolicy.enabled=true without populating pipelock.mcpToolPolicy.rules gets a clear render-time error instead of a container crash-loop with the pipelock validation message. Per CodeRabbit feedback on #1913. * Versions --------- Co-authored-by: Juan José Mata <jjmata@jjmata.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) |
||
|
|
eca8c6ce1f |
fix : account destroyed cascade transfer destruction then … (#1795)
* fix: cascade destroy transfers and reset transaction kind on account destruction. * Add rescue no method to transfer transaction reset --------- Co-authored-by: arumaio <aruma.pro+git@protonmail.com> |
||
|
|
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. |
||
|
|
0497b1d7c1 |
Use date comparisons for interval thresholds (#1923)
* Use date comparisons for interval thresholds Replace hard-coded day counts in Period#interval with direct date comparisons (end_date > start_date + 5.years and + 1.year) for clearer intent and to avoid magic numbers; updated inline comments. No behavioral change intended aside from improved readability. * Use advance(years:) for year-based comparisons Replace start_date + N.years with start_date.advance(years: N) to apply calendar-year semantics (respecting leap years/month boundaries). Update comments to clarify 'calendar years' and the resulting interval choices (monthly for >5 years, weekly for >1 year). Intent is to make the period interval calculation more correct for calendar-aware date comparisons. |
||
|
|
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. |
||
|
|
c8b1d8cf92 |
fix(design-system): DS::Disclosure :default variant summary_content layout (#1929)
PRs #1855, #1857, #1858 (DS::Disclosure :card/:card_inset/:inline variants) introduced a `<div class="w-full">` wrapper around `summary_content`. The wrapper is required for non-default variants — their `<summary>` is `display: list-item` (no flex), so a caller's inner flex+justify-between div would shrink-wrap to content width. But for the `:default` variant, `<summary>` is already `flex items-center justify-between`. Wrapping caller siblings in a single `w-full` block collapses them into one flex child, killing the justify-between distribution. This regressed the only default-variant summary_content caller — `accounts/_accountable_group.html.erb` (the homepage account sidebar) — where the group name and total/sparkline divs no longer aligned across the row. Render `summary_content` bare for `:default` (summary is the flex container) and keep the `w-full` wrapper for `:card`, `:card_inset`, `:inline`. |
||
|
|
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. |
||
|
|
4bb326fee5 |
docs(ds-toggle): warn against external hidden_field_tag with same name (#1925)
DS::Toggle already renders a paired hidden field for the off-state value. Adding an external `hidden_field_tag` with the same `name` in a caller view causes ID/label collisions (the auto-generated id matches the checkbox id, so `<label for=...>` targets the hidden field) and sends duplicate params. Inline ERB comment so the warning surfaces wherever the component is read or copied. |
||
|
|
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.v0.7.1-alpha.10 |
||
|
|
09058b0cc6 |
feat(design-system): extend DS::Pill with badge mode + semantic tones (#1751 PR A) (#1902)
* feat(design-system): extend DS::Pill with badge mode + semantic tones (#1751) Adds two extensions to the existing `DS::Pill` (originally landed as a stage marker primitive in #1829) so it can also serve as the shared status / category badge across the app — the use case tracked by #1751. **Badge mode (`marker: false`)** The original `DS::Pill` was intentionally sub-12px (text-[10px] / text-[11px]) + uppercase + tracking-wide so it reads as a marker (`Beta`, `Canary`, `NEW`), not a label. That shape is wrong for status badges where the surrounding context is regular UI copy and the pill needs to feel like a chip (`Pending`, `Active`, `Past due`, `Failed`). The new `marker: false` flag drops the uppercase + arbitrary sub-12px text and snaps the chrome to the DS text scale: - `marker: false, size: :sm` → `text-xs` (12px), normal case - `marker: false, size: :md` → `text-sm` (14px), normal case - `marker: true` (default) → existing #1829 behavior, unchanged **Semantic tone aliases** Status badges read more naturally with semantic tone names than with the underlying palette colors: | Alias | Resolves to | |---|---| | `:success` | `:green` | | `:warning` | `:amber` | | `:error` / `:destructive` | `:red` (new tone, added here) | | `:info` | `:indigo` | | `:neutral` | `:gray` | Visual-name tones (`:violet`, `:indigo`, `:fuchsia`, `:amber`, `:green`, `:gray`, `:red`) still work as before — semantic aliases resolve through `SEMANTIC_TONE_ALIASES` at component init time, so the callsite can pick whichever name reads better. Unknown tones still fall back to `:violet` (existing behavior). **Red palette** Adds the `:red` tone (palette already present in `design/tokens/sure.tokens.json` — `red-50/100/200/500/700/tint-10`). Needed for `:error` / `:destructive` status badges. **Icon slot** Adds an `icon:` option (already documented in the component's doc-comment as planned). When set, the Lucide glyph replaces the colored dot inside the pill — useful for status badges that read better with a glyph (`circle-check`, `triangle-alert`, `loader`, etc.) than the generic dot. **Scope** API + tests + Lookbook preview only. No callsite migrations in this PR — that's the next slice of #1751, done as separate per-bucket PRs (transaction badges, provider badges, misc) to keep diffs small. DS::Pill currently has no in-app callsites (#1829 shipped the primitive ahead of consumers), so this is a pure-additive change. Existing API is fully backwards-compatible — `marker:` defaults to `true`, so without that flag the pill renders exactly as it does today. * fix(test): use assert_no_selector for dot-suppression assertion `refute_selector ..., count: 1` only fails when there are exactly 1 matches — it would silently pass for 0 OR 2+. The intent is "no dots should render when an icon is set"; `assert_no_selector` strictly asserts zero matches. Flagged by coderabbit on #1902. |
||
|
|
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 |
||
|
|
1f7970010a |
fix(destroy): prevent UnknownAttributeError in DestroyJob rescue (#1852)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> |
||
|
|
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> |
||
|
|
675a7164ed |
fix(helm): skip Redis/DB auto-wiring when extraEnv override is set (#1868)
* fix(helm): skip Redis/DB auto-wiring when extraEnv override is set Setting rails.extraEnv.REDIS_URL or DATABASE_URL caused the chart to emit the URL twice, once from auto-wiring and once from the generic extraEnv loop. Server-side apply rejected the Deployment with a duplicate-key error. The auto-wiring path also added REDIS_PASSWORD / DB_PASSWORD env vars sourced from secret keys that external-service users typically don't have, so even after deduplicating, pods failed CreateContainerConfigError. Gate the auto-emit block on the absence of a user override. The extraEnv loop emits the URL once and no *_PASSWORD env is injected. Chart-managed Redis/Postgres behavior is unchanged. * fix(helm): guard extraEnv index against nil CodeRabbit flagged that `index $ctx.Values.rails.extraEnv "DATABASE_URL"` (and the REDIS_URL equivalent) panic if a user sets `rails.extraEnv: null` or omits the field entirely. Wrap with `default (dict)` first, matching the defensive pattern already used elsewhere in this template. * fix(helm): use hasKey for extraEnv presence check Switch from `(index $railsExtraEnv "URL_KEY")` to `hasKey $railsExtraEnv "URL_KEY"`. With `index`, a user setting `DATABASE_URL: ""` or `REDIS_URL: ""` in extraEnv would still trigger chart auto-injection (empty string is falsy), recreating the duplicate-emission bug for the empty-value case. `hasKey` keys the gate on presence, so any explicit override (including empty) suppresses auto-wiring. |
||
|
|
444f5e6a2d |
optimize net_category_totals() by using memoized cache (#1881)
* optimize net_category_totals() by using memoized cache * fix issue - net_category_totals cache is never populated - suggested by coderabbitAI |
||
|
|
ba87a4f0f3 |
fix(imports): prevent MissingTemplate for JSON requests in index (#1875)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> |
||
|
|
fc5cc26812 |
fix(entry): Guard entryable_attributes for non-transaction entries in bulk_update! (#1877)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.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. |
||
|
|
25bb394378 |
fix(design-system): DS::Select a11y — fix aria-expanded, listbox keyboard nav, label binding (#1848)
* fix(design-system): DS::Select a11y — fix aria-expanded, listbox keyboard nav, label binding Closes #1744. Several concrete bugs from the savings-goals audit: 1. **`aria-expanded` wired to the wrong state.** The template had `aria-expanded="<%= @selected_value.present? ? "true" : "false" %>"`, which is "has a value been chosen", not "is the menu open". AT users heard a misleading signal on every page load. Init to `"false"`; the Stimulus controller's openMenu/close already correctly maintains the attribute after that. 2. **`aria-labelledby` referenced a nonexistent id.** The trigger pointed at `"#{method}_label"`, but the rendered `<label>` had no id at all — the binding silently failed. Add `id: "#{method}_label"` to `form.label` so the reference actually resolves to the label text. Only emit `aria-labelledby` when there *is* a visible label. 3. **`tabindex="0"` on every option.** Listbox options should use roving tabindex (only the selected option is in tab order; the rest are reachable via ArrowUp/Down). Set `tabindex="0"` on the selected option only; `"-1"` on the rest. The select controller's `select()` handler keeps the roving invariant on user interaction. 4. **No keyboard navigation between options.** Add ArrowDown/Up (cycle), Home (first), End (last). The existing Enter/Escape handlers stay. ArrowUp/Down inside the search input is left alone so the input's caret behavior isn't hijacked. 5. **Search input had no accessible name.** Add an explicit `aria-label` matching the placeholder copy so AT users hear "search" when focus enters the field. API unchanged. Builder-level routing fix in `StyledFormBuilder#select` (calling DS::Select for `f.select(...)` the same way `f.collection_select` already does) is intentionally out of scope — it's a separate translation pass for the choices format. Documented as a follow-up. * fix(review): bridge search input to visible options in DS::Select ArrowDown/Up from the search input now focus the first/last visible option, and keyboard navigation operates on visible options only. After typing a search query, the controller promotes the first visible option to tabindex="0" so Tab can land on it even when the previously tab-eligible option is filtered out. Addresses Codex review on PR #1848 (issue #1744). * fix(review): include trigger in DS::Select aria-labelledby Codex P2 follow-up on #1848: \`aria-labelledby=\"#{method}_label\"\` makes the trigger button's accessible name come solely from the external form label — that overrides the button's own text node (\`selected_item[:label]\` / placeholder). Screen readers therefore announce only "Currency" without ever hearing the selected "USD" unless the user opens the listbox. Give the trigger \`id=\"#{method}_trigger\"\` and reference both ids: \`aria-labelledby=\"#{method}_label #{method}_trigger\"\`. The accessible-name algorithm concatenates the two, so AT users now hear \"<Label> <selected value>\" while \`aria-expanded\` / \`aria-haspopup\` continue to convey the dropdown state. |
||
|
|
56ff8513cb |
fix(design-system): DS::Tabs a11y — WAI-ARIA tab pattern + keyboard nav (#1847)
* fix(design-system): DS::Tabs a11y — WAI-ARIA tab pattern + keyboard nav Closes #1745. DS::Tabs rendered as a bare `<nav>` + `<button>` list with no role wiring. AT users would hear "navigation, button, button, button" instead of the tab semantics. Keyboard users got no arrow-key nav between tabs. Five fixes: 1. **Role scaffolding.** `<nav>` → `role="tablist"`, `aria-orientation="horizontal"`. Each tab `<button>` → `role="tab"`, `aria-selected`, `aria-controls="panel-#{id}"`. Each panel `<div>` → `role="tabpanel"`, `id="panel-#{tab_id}"`, `aria-labelledby="#{tab_id}"`, `tabindex="0"` (so the panel itself is reachable via keyboard for in-panel content nav). 2. **Roving tabindex.** Active tab is `tabindex="0"`, inactive are `tabindex="-1"`. ArrowLeft/Right cycles focus across the tablist without leaving the widget; Tab jumps past the whole widget. Stimulus controller updates both `aria-selected` and `tabindex` on tab switch. 3. **Manual activation.** Per WAI-ARIA APG "Tabs with Manual Activation" — arrow keys MOVE focus, Enter/Space ACTIVATES the focused tab. Avoids accidental tab swaps when the user is just navigating. Important here because several tab contents trigger Turbo fetches (transactions index, account sidebar, budgets). 4. **Home/End shortcuts.** Home jumps focus to the first tab, End to the last. WAI-ARIA APG-standard. 5. **Raw palette → token.** Replace `bg-white theme-dark:bg-gray-700` on the active button with the existing `tab-item-active` utility (defined in `_generated.css` from `design/tokens/sure.tokens.json`). Single class, dual-mode. Also gate the transition behind `motion-safe:` so reduced-motion users get an instant snap. API unchanged — the slot signatures (`btns(id:, label:)`, `panels(tab_id:)`) take the same args. Caller-provided `id:` is still the public identifier; `panel-#{id}` is internal naming for the `aria-controls`/`aria-labelledby` pair. * fix(review): scope DS::Tabs DOM ids to component instance Per CodeRabbit review on #1847: raw `panel-#{tab_id}` and `id: tab_id` on buttons collide when multiple DS::Tabs widgets on the same page share generic tab ids (e.g., "all", "overview", "transactions"), breaking aria-controls / aria-labelledby associations. Scope ids via per-instance `dom_prefix` ("tabs-#{object_id}") and share the same prefix between DS::Tabs and DS::Tabs::Nav so button ids and panel labelledby/controls stay consistent. * fix(review): use <div> host for role=tablist in DS::Tabs::Nav Codex P2 follow-up on #1847: \`<nav>\` has a fixed landmark role per ARIA-in-HTML and may not be repurposed as a tablist. The current \`tag.nav class: ..., role: \"tablist\"\` produces invalid markup — some AT implementations ignore the role override, in which case the child \`role=\"tab\"\` buttons end up without a valid tablist parent and the keyboard / AT contract this PR is meant to add silently regresses. Swap the container for a neutral \`tag.div\`. Tab semantics (\`role\`, \`aria-orientation\`, keyboard nav, manual-activation pattern) are unchanged. |
||
|
|
7a0cafd6ba |
fix(design-system): DS::Dialog a11y — role, aria-modal, aria-labelledby, heading_level (#1846)
* 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. |
||
|
|
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> |
||
|
|
f2782901d3 |
fix(design-system): DS::Link a11y — distinguishable default, icon-only label, external-link hardening (#1844)
* fix(design-system): DS::Link a11y — distinguishable default, icon-only label, external-link hardening Closes #1739. DS::Link extends Buttonish, so the styled variants (`:primary`, `:secondary`, `:icon`, `:ghost`, etc.) inherit the Buttonish styling pipeline. The `default` variant is the bare inline link, which had multiple a11y gaps: 1. **WCAG 1.4.1 — color is not the only difference.** The default variant had `container_classes: ""`, so a link rendered as plain text-color text with no underline, no weight change, nothing. Color-only differentiation fails WCAG 1.4.1 for low-vision and colorblind users. Now: `text-link underline underline-offset-2 hover:no-underline` — underlined at rest, underline removed on hover for a polish hint, plus the `text-link` token (blue-600 light / blue-500 dark) for color. 2. **Focus ring.** `<a>` doesn't pick up the `button` focus rule from base.css (#1738). Add `focus-visible:outline-2 outline-offset-2 outline-gray-900 theme-dark:outline-white` directly on the default variant. The Buttonish-derived variants render as buttons visually but as `<a>` in markup — out of scope here; covered by their own callsites styling. 3. **Icon-only accessible name.** Mirror the DS::Button fix from #1738: derive a humanized `aria-label` from the icon key when the caller doesn't provide one, so AT users hear "More horizontal" instead of just the URL. 4. **External-link hardening.** `target="_blank"` without `rel="noopener"` exposes `window.opener` to the new tab (reverse-tabnabbing). Always set `noopener noreferrer` when the target is `_blank`. Authors can override by passing `rel:` explicitly. 5. **sr-only "(opens in new tab)" hint.** Append an `sr-only` span after the link text when `target="_blank"` so AT users hear the navigation behavior. Visual indication (e.g. an external-link icon) stays at the caller's discretion. Locale key: `ds.link.opens_in_new_tab` (en only — other locales in a separate translation pass per repo norm). API unchanged. No existing callsites use `target="_blank"` or icon-only links, so no migration needed. * fix(review): fold new-tab cue into icon-only aria-label When an icon-only DS::Link also targets `_blank`, the generated `aria-label` was overriding the descendant accessible name, masking the sr-only "(opens in new tab)" span. Include the cue directly in the generated label so AT users hear the warning. Also switch `capitalize` to `humanize` so multi-word icon keys like `external-link` read as "External link" rather than "External link" already worked but `humanize` is the more idiomatic Rails choice and keeps us aligned with the suggested patch. Flagged by Codex P2 + CodeRabbit on PR #1844. * fix(review): swap raw outline palette to alpha-ring tokens Codex P1 follow-up after the ready-for-review transition: the default \`DS::Link\` focus ring used raw \`outline-gray-900\` + \`theme-dark:focus-visible:outline-white\`, which violates the DS-hygiene rule that bans raw Tailwind palette utilities in component styling. Swap to the established alpha-ring pattern already used by DS::Toggle (#1843), DS::Tooltip (#1845), provider_card, and form-field — \`focus-visible:ring-2 focus-visible:ring-alpha-black-300\` + \`theme-dark:focus-visible:ring-alpha-white-300\`. Same visual contract (WCAG 1.4.11), theme tokens centralized. |
||
|
|
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). |
||
|
|
272b8acd66 |
feat(theme): broadcast theme:change so SVG/canvas consumers can repaint (#1839)
`theme_controller#setTheme` already toggles `data-theme` on the
document element, but D3/SVG/canvas consumers that bake color into
attributes (`fill`, `stroke`, `stop-color`) can't observe a CSS
variable change — they need an imperative re-render hook.
Dispatch a `theme:change` CustomEvent on the document element after
the attribute flips, with `detail: { theme: "dark" | "light" }`.
Consumers subscribe via standard connect/disconnect listeners.
Refactor the if/else into a single path while at it — same behavior,
half the lines.
Closes #1764.
|
||
|
|
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 |
||
|
|
34d6f4d8d6 |
fix(design-system): DS::Disclosure focus ring + motion-safe chevron (#1841)
Closes #1741. Two small a11y polishes on the native `<details>` / `<summary>` primitive: - Add a token-backed focus-visible ring on `<summary>`. Previously inherited only the browser default outline, which was thin and inconsistent across engines. Match the new pattern from #1738: `outline-2 outline-offset-2 outline-gray-900` plus `theme-dark:outline-white` so the ring lands on the page chrome outside the disclosure regardless of mode. (WCAG 2.4.7.) - Gate the chevron rotation behind `motion-safe:transition-transform` + `motion-safe:duration-150`. The chevron now slides between closed/open states for users who haven't opted out of motion, and snap-rotates instantly under `prefers-reduced-motion: reduce`. (WCAG 2.3.3, AAA.) While here: drop the no-op `group-open:transform` class. Tailwind v4 applies `rotate-90` / `rotate-180` directly without needing the explicit `transform` utility — it was a v3 holdover. |
||
|
|
51b0336262 |
fix(design-system): DS::FilledIcon decorative-vs-meaningful API (#1842)
* fix(design-system): DS::FilledIcon decorative-vs-meaningful API Closes #1742. `DS::FilledIcon` is mostly used as a decorative visual indicator next to a textual label (transaction merchant avatar, recurring-transaction icon, payment-method tile, etc.). The wrapper was rendering without any aria scaffolding, so screen readers had to traverse the inner `<svg>` or single-letter `<span>` with no context. Two new kwargs: - `description:` (nil) — when set, the wrapper emits `role="img" aria-label="<description>"`. Use this when the surrounding DOM does not carry the label (e.g. icon-only badges in a grid). - `aria_hidden:` (auto) — defaults to `true` when `description:` is blank (= decorative), `false` when description is present. Pass explicitly to override for the rare case where you want the visual exposed but the name already lives in adjacent text. API stays backwards-compatible: existing 33 callsites get `aria-hidden="true"` by default, which is correct — the visible text next to the icon already carries the name. While here: doc the `:text` variant gotcha — only `text.first` is rendered, so AT users hearing "A" can't infer "Apple". Callers should pass the full `description:` when relying on this variant. Out of scope (filed elsewhere if needed): - Touch-target audit (decorative wrapper, WCAG 2.5.5 doesn't apply). - `hex_color:` palette soft-validation (would require a token-name registry; deferred until #1736 / #1653 land). - `color-mix(in oklab, ...)` browser-support note for the transparent variant — tier-2 concern. * fix(review): gate role/aria-label when hidden, normalize blank description CodeRabbit feedback on #1842: - Avoid emitting role="img" and aria-label alongside aria-hidden="true" (dead markup; AT ignores semantics on hidden subtrees). - Normalize blank description strings to nil via .presence so the default aria_hidden fallback treats "" the same as nil. * fix(review): use description.presence so aria-label drops blank strings Codex follow-up review 4319747515 caught that the prior fix still emitted `aria-label=""` when description was a blank string. `.presence` returns nil for blank — Rails `tag.div` drops the attribute entirely when the value is nil. |
||
|
|
e56ad3de42 |
fix(design-system): DS::Toggle focus ring, role=switch, and semantic tokens (#1843)
* fix(design-system): DS::Toggle a11y + token swaps Closes #1746. Four fixes on the toggle primitive (visual switch backed by a sr-only checkbox). 1. **Focus ring (WCAG 2.4.7)** — the `<input>` is `sr-only`, so the browser-default focus ring lands on an invisible 0px element. The label (the track) had no focus styling, meaning the component had **no visible focus indicator at all**. Add `peer-focus-visible:ring-2 ring-offset-2 ring-gray-900` with `theme-dark:peer-focus-visible:ring-white` so the ring appears on the visible track when the underlying checkbox receives keyboard focus. 2. **Role semantics** — visual is a switch, but the element was announced as "checkbox, checked" because the native input is a checkbox. Add `role="switch"` so AT users hear "switch, on" / "switch, off". `aria-checked` is inherited from the checkbox's checked state, no manual wiring needed. 3. **Token swaps** — replace raw palette references with semantic tokens: - Track `bg-gray-100 theme-dark:bg-gray-700` → `bg-surface-inset` - Checked `peer-checked:bg-green-600` → `peer-checked:bg-success` Picks up the contrast bump from #1735 automatically. 4. **Motion safety (WCAG 2.3.3)** — gate the bg color + thumb-translate transitions behind `motion-safe:`. Reduced-motion users see an instant state snap; everyone else gets the existing 300ms ease. API unchanged. Existing 8 callsites (settings/preferences, settings/appearances, account_sharings, budgets/edit, recurring_transactions, styled_form_builder bridge) work without changes. * fix(review): use alpha tokens for Toggle focus ring Swap raw palette (ring-gray-900 / theme-dark:ring-white) on the DS::Toggle focus ring to ring-alpha-black-300 / ring-alpha-white-300 to match the focus-ring token pattern already used by form-field, provider_card, and shared/_badge. Closes AI review feedback on #1843. |
||
|
|
04ba4dd28f |
fix(design-system): bump --color-success for WCAG 1.4.11 contrast (#1838)
Light: green-600 (#10A861) -> green-700 (#078C52). Lifts the success icon on `bg-success/10` from 2.77:1 to 3.77:1, clearing the WCAG 1.4.11 3:1 minimum for non-text UI components. Dark: green-500 (#12B76A) -> green-400 (#32D583), keeping the warning/destructive 600-light/400-dark step pattern intact and moving from 5.95 to 7.90. Source change in design/tokens/sure.tokens.json; _generated.css regenerated via `npm run tokens:build`. Closes #1735. Resolves #1736 child #4. |