mirror of
https://github.com/we-promise/sure.git
synced 2026-05-30 07:49:01 +00:00
8cf6a7a4ca978fdc39f1f857089084ff310e262b
2815 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8cf6a7a4ca |
refactor(goals): migrate index search field to DS::SearchInput
#1853 just landed on `main` (`8e444ff9`), so the goals index search input can move off the hand-rolled markup and onto the new `DS::SearchInput` primitive. Behaviour unchanged — the `data-goals-filter-target="input"` and `data-action="input->goals-filter#filter"` hooks pass through via the component's `data:` option, and the wrapper's `flex-1 min-w-[200px]` carries through via the component's `class:` arg. Drops the broken `focus:ring-gray-500` lookalike (the hand-rolled class used `focus:ring-alpha-black-100` which is fine, but the new primitive uses the canonical `focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-900` pattern from DS::Button — same focus contract everywhere). Addresses sure-design's DS Drift Patrol finding on #1798. |
||
|
|
903121f66b |
Merge origin/main into feat/goals-v2-architecture
Pulls in #1853 (DS::SearchInput), #1855 (DS::Disclosure :card), #1856 (3 provider panels migration) so the goals views can migrate to the new primitives. |
||
|
|
709b7bf91b |
fix(goals): replace ring-gray-500 with semantic ring-alpha-black-500 token
The colour-picker swatches were using the raw Tailwind palette utility `peer-checked:ring-gray-500`. Swap for the design-system functional token `peer-checked:ring-alpha-black-500` so the focus ring inherits the same alpha-on-surface treatment used elsewhere in the system and respects dark mode. Flagged by sure-design's DS Drift Patrol on #1798. |
||
|
|
48862f8ed9 |
refactor(goals): use semantic color tokens for ring + status callout
`Goals::CardComponent#ring_color` and `goals/_status_callout` reached into the Tailwind palette directly (`text-yellow-700`, `var(--color-green-600)`, etc.) for status-coded colors. The sure-design-system already exposes the matching semantic tokens (`text-warning`, `text-success`, `--color-success`, `--color-warning`), which theme-swap correctly in dark mode and survive palette renames without view edits. - `ring_color`: collapse `:reached` / `:on_track` to `--color-success` (the status pill already differentiates them via icon — completed star vs check) and `:behind` to `--color-warning`. The `:no_target_date` fallback keeps `--color-gray-400` for now since there's no semantic neutral token; that gets cleaned up alongside the DS::ProgressRing extraction. - `_status_callout`: drop `text-yellow-700 theme-dark:text-yellow-300` and `text-green-700 theme-dark:text-green-300` for the equivalent semantic `text-warning` / `text-success` utilities. No visual regression in light mode (success collapses two adjacent greens into one); dark mode now properly inverts via the design system's theme variants instead of hand-rolled overrides. The `stroke="var(--budget-unused-fill)"` track on the inline card ring stays for now — that's a token-rename refactor that touches budget code outside this PR's scope and lands cleanest with the DS::ProgressRing primitive that consolidates the three ring implementations. |
||
|
|
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> |
||
|
|
e16d9c4a6a |
Merge origin/feat/goals-v2-architecture; reconcile beta→preview rename
Remote branch added a beta_gated_nav_item helper + 'Gating the main nav' docs section. Main concurrently renamed the beta-features gate to preview-features (concern, predicate, JSONB key, locale flash). Rename the new helper / partial local / pill marker to match preview naming and port the nav-gating docs into gating-a-preview-feature.md so the improvement survives the rename. Resolved conflicts: - db/schema.rb: take the later schema version (2026_05_19_100000). - docs/llm-guides/gating-a-beta-feature.md: accept main's deletion; port the 'Gating the main nav' section into the preview guide. Renames carried through to keep the gate wired end-to-end: - application_helper.rb: beta_gated_nav_item → preview_gated_nav_item; beta_features_enabled? → preview_features_enabled?; beta: → preview:. - _nav_item.html.erb: beta: local → preview: local; shared.beta i18n key → shared.preview. - application.html.erb: caller renamed to preview_gated_nav_item. - goals/index.html.erb: pill label uses shared.preview. - shared/en.yml: 'beta: Beta' → 'preview: Preview'. - goals_controller, goal_pledges_controller: require_beta_features! → require_preview_features!. - goals_controller_test, goal_pledges_controller_test: flip the preference key, flash matcher, and test names to 'preview'. |
||
|
|
926d71c74a |
Merge origin/main into feat/goals-v2-architecture
Resolved conflicts:
- db/schema.rb: take main's schema version (later migration timestamp);
goals + debug_log_entries tables both present.
- app/views/categories/_form.html.erb: keep branch's shared
color-icon-picker controller action; adopt main's t('.auto_adjust') i18n.
|
||
|
|
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 |
||
|
|
663a624cc9 | fix(preview): surface demo data failure logs (#1865) | ||
|
|
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. |
||
|
|
e8ce28648d |
refactor: rename beta features gate to preview features (#1837)
* refactor: rename beta features gate to preview features Renames the opt-in gate introduced in PR #1829 from "beta" to "preview". Same shape (per-user JSONB toggle, `before_action` concern, marker pill) just retitled so the surface speaks the language Sure uses elsewhere ("preview" reads as in-progress, "beta" had baggage with provider maturity copy and external testing programs). Renames: - BetaGateable -> PreviewGateable - require_beta_features! -> require_preview_features! - beta_features_enabled? -> preview_features_enabled? - preferences["beta_features_enabled"] -> preferences["preview_features_enabled"] - DS::Pill default label "Beta" -> "Preview" - Settings -> Preferences toggle copy "beta features" -> "preview features" - config/locales/views/beta/ -> config/locales/views/preview/ - docs/llm-guides/gating-a-beta-feature.md -> gating-a-preview-feature.md Includes a data migration that copies any existing `beta_features_enabled` JSONB key into `preview_features_enabled` so early opt-ins survive the rename, then removes the old key. The migration is fully reversible. Provider maturity copy ("maturity.beta = Beta" under Settings -> Bank sync) is intentionally untouched - that's a separate concept describing a provider's integration stability, not Sure's feature gate. * review: apply CodeRabbit findings on PR #1837 - Settings::PreferencesController#update now routes the `preview_features_enabled` input through strong params and casts via ActiveModel::Type::Boolean instead of reading raw params and string- comparing to "1". Matches Sure's controller convention for permitted params and avoids stringly-typed boolean handling. - Rename migration now wraps the destination JSONB key write in COALESCE so a row that somehow ends up with both keys keeps the destination value instead of having it overwritten by the source. Up and down paths get the same defensive shape. * 📝 CodeRabbit Chat: Implement requested code changes * 📝 CodeRabbit Chat: Implement requested code changes * fix: restore all missing translation keys; rename beta→preview label * fix: restore all missing sections (appearances, debugs, llm_usages, providers, etc.); rename beta→preview * fix: restore missing keys (member_removal_failed, confirm_delete, etc.); add preview section * fix(i18n/ca): use 'està en vista prèvia' instead of 'és una vista prèvia' * fix(i18n/ca): use 'en desenvolupament'; drop article in preview title * fix(i18n/es): use 'en desarrollo' instead of 'en progreso' * fix(i18n/ca): use 'funcions experimentals' instead of 'vista prèvia' * fix(i18n/es): use 'funciones experimentales' instead of 'vista previa' * fix(i18n/ca): use 'funcions experimentals' in preferences.show.preview * fix(i18n/es): use 'funciones experimentales' in preferences.show.preview * fix(i18n/ca): use 'Experimental' pill label instead of 'Vista prèvia' * fix(i18n/es): use 'Experimental' pill label instead of 'Vista previa' --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
65bed0db2f |
i18n(en): hardcode 'One error' in sso_providers errors_title singular form (#1854)
* i18n(en): drop %{count} interpolation in sso_providers errors_title 'one' branch
Per @jjmata review feedback on #1830: when count == 1, English reads
more naturally as 'One error prohibited...' than '1 error prohibited...'.
The 'other' branch keeps %{count} so '2 errors...', '3 errors...' still
interpolate.
* Update en.yml
Signed-off-by: Guillem Arias Fauste <accounts@gariasf.com>
---------
Signed-off-by: Guillem Arias Fauste <accounts@gariasf.com>
|
||
|
|
1ddd8bd040 |
feat(i18n): complete Catalan translations + extract residual hardcoded strings (#1836)
* feat(i18n): complete Catalan translations + extract residual hardcoded strings
CA coverage
- All view/model/breadcrumb/doorkeeper/mailer locale files for ca: 0 missing
keys (was ~3,400). Translations follow informal "tu" register, sentence case,
domain glossary (Compte/Saldo/Transacció/Posició/Operació/Pressupost/...).
- Catalan pluralization test: ca uses one/other; mirrors
test/lib/polish_pluralization_test.rb.
- 8 LanguageTool-flagged grammar fixes applied (Connexió òrfena, Secret de
l'API, comma-pero, apostrophe elisions, etc).
Hardcoded string extraction (also fixes EN parity)
- UI::Account::Chart#title + chart.html.erb view tabs -> UI.account.chart.*
- UI::Account::BalanceReconciliation labels + tooltips ->
UI.account.balance_reconciliation.{labels,tooltips}.*
- transactions/_transfer_match.html.erb (Auto-matched, A/M, Confirm/Reject
match, Payment/Transfer is confirmed) -> transactions.transfer_match.*
- AccountOrder labels (Name/Balance asc/desc) -> account_order.* keys with
fallback to existing hardcoded labels.
- Depository::SUBTYPES surface in account list -> depositories.subtypes.*.*
- User role badge -> users.roles.* (admin / member / super_admin).
- 110+ country names -> countries.* (config/locales/countries.ca.yml).
Breadcrumb locale fix
- Breadcrumbable was a before_action that ran before Localize's around_action
switched I18n.locale, so default crumbs rendered in EN even when locale=ca.
- Convert to helper_method that defers translation to render-time (when
I18n.locale is already correct). Add all missing breadcrumb keys to ca + en.
- Layouts switched from @breadcrumbs to breadcrumbs helper.
Locale-aware helpers / formatters
- ApplicationHelper#localized_ordinal: ordinalize that respects ca
(1r/2n/3r/4t/Nè). Wired into preferences month_start_day select.
- Family#moniker_label / moniker_label_plural: translate the default "Family"/
"Group" monikers via shared.family_moniker.* with fallback to the family's
custom override.
- Budget#name: use I18n.l for month_year/short/long instead of strftime("%B %Y")
so the budget header date follows the active locale.
Tooling
- script/lt_check_ca.rb: batched LanguageTool checker (premium endpoint when
LT_USERNAME/LT_API_KEY are set, free fallback otherwise), picky mode,
motherTongue=en for false-friend detection.
- lib/tasks/i18n_screenshot.rake: dev-only rake to set user.locale=ca and
role=super_admin on the demo user so the i18n surfaces can be walked.
Out of scope (pre-existing, not introduced here)
- Native browser file input "Choose Files / No file chosen" (browser locale).
- D3.js client-side chart x-axis dates (JS-side Intl.DateTimeFormat needed).
- Sankey/donut labels = seed category names (data, not i18n).
- 2 rails-i18n datetime/errors interpolation warnings inherited from
config/locales/defaults/ca.yml.
* fix(i18n): apply idiomatic Catalan review (3-agent + native review)
Three parallel review agents flagged 203 findings (31 high / 73 medium / 99 low)
across all 111 ca.yml files. This commit applies the high-severity bugs plus a
curated subset of medium-impact fixes.
Grammar / agreement
- provider_sync_summary.health.stale_pending: `(exclòs)` -> `(exclosa/excloses)`
to agree with feminine `transacció(s)`.
- accounts.confirm_unlink.warning_no_sync: added reflexive `es` -
`el compte ja no es sincronitzarà`.
- sophtron_setup_required.heading: `no configurats` -> `sense configurar`
(avoids broken agreement across "ID" masc. + "clau" fem.).
- admin.sso_providers.form.errors_title: split into one/other pluralization
keys (en + ca); singular `ha impedit` was wrong for count > 1.
Brand consistency
- IndexaCapital -> Indexa Capital (37 occurrences across one file).
- Lunchflow -> Lunch Flow in two remaining places.
Anglicisms / domain mistranslations
- kraken_items setup_accounts.instructions: `ompliments d'operacions`
(lit. dental/food fillings) -> `execucions d'operacions`.
- settings kraken_panel.read_only_title: `Sincronització d'intercanvi`
(swap/trade) -> `Sincronització només de lectura amb l'exchange`.
- transactions convert_to_trade.security_custom + security_not_listed_hint:
`cotització` (price quote) -> `ticker` (the EN field IS a ticker symbol).
- loans.form.rate_type: `Tipus d'interès` collided with sibling
interest_rate -> `Modalitat del tipus`.
- brex_items.provider_panel.sandbox_note_html: `L'staging` (broken
contraction) -> `el staging`.
Idiom traps
- coinbase/binance/kraken wait_for_sync: `acabi de sincronitzar` is
ambiguous in CA (`acabar de + inf` reads as "has just done X") ->
`acabi la sincronització`.
- chats.ai_greeting.there: `a tothom` -> `''` (the EN fallback "Hey there"
is singular; literal CA `tothom` is plural and wrong for 1:1 chat).
- transactions.split_parent_row.split_label: `Divideix` (imperative) is
wrong as a status badge -> `Divisió` (noun).
- transactions.keep_both (2 occurrences): infinitive `mantenir ambdues` ->
imperative `mantén-les totes dues` to match the sibling Yes/No buttons.
- rules.clear_ai_cache: `Reinicia` (restart) -> `Buida` (empty/clear),
which matches the success notice (`s'està netejant`).
Moniker gender breakage (cross-file)
%{moniker} is interpolated downcased from family.moniker_label and may
resolve to feminine `família`/`llar` or masculine `grup`. Strings that
hard-code a gendered article ('al teu %{moniker}', 'aquesta %{moniker}',
'aquest/a %{moniker}') broke on at least one branch. Restructured the
affected sentences to drop the gendered determiner:
- account_sharings.show.no_members
- merchants.family_empty / family_title / provider_empty
- registrations.new.join_family_title
- settings.preferences.show.currencies_subtitle / sharing_subtitle
- simplefin_items.select_existing_account.no_accounts_found
- invitations.new.subtitle
- invitation_mailer.invite_email.subject (mailers/) + body (views/)
- snaptrade_items.providers.snaptrade.free_tier_warning
Terminology consistency
- models/account_statement/ca.yml attributes aligned with view-side
forms: `Saldo d'obertura`/`Saldo de tancament` ->
`Saldo inicial`/`Saldo final`; `Suggeriment de...` -> `Pista de...`.
- account_statements.coverage.status.not_expected:
`No s'esperava` -> `No previst` (status label, not past action).
- account_statements.index.empty_unmatched: aligned with the section's
own label `Safata sense aparellar`.
- imports.create.document_provider_not_configured + document_upload_failed:
`arxiu vectorial` -> `magatzem vectorial` (correct TermCat term).
- coinstats_items blockchain gender: `els blockchains` / `un blockchain` ->
`les blockchains` / `una blockchain` (feminine per TermCat).
- accounts.account.remove_default: `Treu el predeterminat` ->
`Treu com a predeterminat` (pairs with sibling `Estableix com a
predeterminat`).
- accounts.tax_treatments.tax_deferred: `Diferit fiscalment` (lit. calque)
-> `Tributació diferida` (standard CA tax-accounting term).
- settings.payments.show.currently_on_plan: `Actualment al` ->
`Actualment al pla:` (was a fragment).
Out of scope (review flagged, not applied here)
- LOW-severity stylistic preferences (Veure vs Mostra, etc).
- `models/category/ca.yml` default category names — seeded at family
creation, not via I18n at runtime, so changes wouldn't affect existing
families.
- `models/period/ca.yml` short labels mixing EN (MTD/YTD) and CA (STD/MA)
— needs a one-convention decision separately.
* fix(i18n,ca): drop gendered article in period_activity + tighten cash-flow terms
- pages.dashboard.investment_summary.period_activity: 'Activitat del
%{period}' contracted 'del' = 'de el' (masc.sg.). %{period} resolves
to mixed forms ('Setmana en curs' fem, 'Últims 30 dies' pl., 'Any en
curs' apostrophe), so hard-coded 'del' was wrong on most labels.
Replaced with 'Activitat — %{period}' (em-dash) to skip the
contraction entirely.
- pages.dashboard.outflows_donut.title / total_outflows: switched from
bare 'Sortides' / 'Total de sortides' to 'Sortides de caixa' /
'Total de sortides de caixa' to match TermCat's precise term
('sortida de caixa' = cash outflow).
* fix(i18n,ca): rephrase transfer source/destination amount labels
'Import d'origen' / 'Import de destinació' were literal calques of
'Source amount' / 'Destination amount'. In a multi-currency transfer
form (sender/receiver in different currencies) the natural CA pair is
'Import enviat' / 'Import rebut'.
* fix(i18n,ca): 'Dades en brut' -> 'Dades sense processar'
The literal calque of 'Raw data' read as too technical for personal-
finance UI. 'Dades sense processar' is the more natural Catalan
equivalent for raw/unprocessed data files.
* fix(i18n): localize Import col_sep label + separator options
The CSV upload form rendered 'Col sep' (the auto-humanized attribute
name) plus hardcoded English 'Comma (,)' / 'Semicolon (;)' options
from Import::SEPARATORS.
- activerecord.attributes.import.col_sep added (en + ca: 'Column
separator' / 'Separador de columnes').
- Import.separator_options class method returns translated tuples;
view switched from Import::SEPARATORS to Import.separator_options.
- activerecord.attributes.import.col_seps.{comma,semicolon} added so
the option labels follow the active locale.
* fix(i18n,ca): drop moniker apposition in sharing/currencies section titles
- sharing_title 'Compartició de %{moniker}' rendered as 'Compartició
de Família' (a noun-noun apposition that's odd in CA) -> 'Compartició
de comptes'.
- sharing_subtitle replaced '%{moniker}' with 'entre els membres' so
the sentence reads naturally and doesn't depend on moniker gender.
- currencies_title 'Divises de %{moniker}' had the same apposition
-> 'Divises'. Subtitle no longer references moniker either.
* fix(i18n,ca): keep 'Self Hosting' untranslated
Reverted 'Autoallotjament' / 'autoallotjada' / 'autoallotjats' usages
to the original English 'Self Hosting' (sidebar label, breadcrumbs,
hostings page title, chat assistant settings hint, redis configuration
subheading, LLM usages cost-estimates description).
The brand-style term reads more naturally in EN for technical users
configuring their own deployment.
* fix(i18n,ca): lowercase 'self hosting' (sentence case in labels)
* fix(i18n): extract budget_categories stepper + allocation_progress strings
Hardcoded English strings on the budget category editor:
- 'Setup' / 'Categories' stepper labels in budgets/_budget_nav.html.erb
- 'X% set' / '> 100% set' / 'left to allocate' / 'Budget exceeded by ...'
in budget_categories/_allocation_progress.erb
- '/m avg' caption + 'Shared' placeholder + 'Leave empty to share
parent's budget' tooltip in budget_categories/_budget_category_form
and _uncategorized_budget_category_form
Extracted to:
- budgets.budget_nav.{setup,categories}
- budget_categories.allocation_progress.{percent_set,over_set,left_to_allocate,budget_exceeded_html}
- budget_categories.budget_category_form.{monthly_average,shared_placeholder,shared_title}
CA translations added; EN keys mirror the prior literals.
* chore(i18n): drop translation tooling from PR
These were dev-only helpers used during the Catalan translation pass:
- script/lt_check_ca.rb: LanguageTool API checker (premium/free
endpoint, picky mode, batching). Useful for ongoing locale QA but
shouldn't ship in this feature PR.
- lib/tasks/i18n_screenshot.rake: rake task that flips user.locale and
role on the demo user for walking the i18n surfaces locally.
Both stay available locally; pulled out of the PR scope.
* fix(i18n): apply PR review feedback (CodeRabbit + Codex)
- balance_reconciliation crypto_items: use :end_balance_crypto tooltip
(was :end_balance_investment). Added new UI.account.balance_reconciliation.tooltips.end_balance_crypto key in en + ca.
- doorkeeper.ca.yml confidentiality.no: was YAML boolean false, now string 'No'.
- views/categories: 'Poor contrast, choose darker color or' continued with hardcoded 'auto-adjust.' button text; extracted to categories.form.auto_adjust key (en + ca).
- imports.create.document_upload_failed: 'a l'magatzem' was broken
contraction -> 'al magatzem'.
- invitation_mailer body + mailer subject: 'unir-se' -> 'unir-te' (was
3rd person, should be 2nd to match the rest of the copy).
- 7 strings across mercury_items / sophtron_items / simplefin_items /
lunchflow_items / brex_items / indexa_capital_items / other_assets:
'se sincronitzaran' -> 'es sincronitzaran', 'se segueixen' ->
'es segueixen' (correct reflexive pronoun before consonants).
- settings.providers.status: key was 'false' (YAML-coerced), now 'off'
to match settings/en.yml status.off used in view lookups.
- sophtron_items.sophtron_setup_required.message: stripped trailing
blank line from the quoted scalar.
- settings/profiles/show.html.erb: switched 'family_moniker ==
"Group"' branch checks to 'Current.family&.moniker == "Group"'.
After Family#moniker_label started returning translated values,
callers using the display label for branching would render the
household copy for group families in ca. Compare the stored sentinel
instead.
- Did not apply CodeRabbit's webauthn 'eliminada' -> 'desada' suggestion:
the key is wired to the destroy action (verified at
settings/webauthn_credentials_controller.rb:55), so 'eliminada' is
correct.
|
||
|
|
91b9d368e4 |
fix(goals): card hover uses bg-container-hover (gray-50) instead of shadow lift
Mirror the affordance used in the accounts/select_provider screen and the bank-sync flows: a near-imperceptible gray-50 fill swap rather than a shadow. Lighter than the previous shadow lift, doesn't introduce elevation noise inside the grid, and the ring track (gray-200) + status pill outline still keep enough separation against the gray-50 hover bg that we don't reintroduce the original contrast issue. |
||
|
|
aa67f84c21 |
fix(goals/demo): seed goals in generate_new_user_data_for!
Cloudflare preview entrypoint, FamilyResetJob, and the Settings "Reset + load sample data" flow all go through generate_new_user_data_for!, which seeded categories/accounts/transactions/budget but not goals. Move generate_goals! inside this method (alongside the same call already in generate_default_data!) so every sample-data surface gets the full state-coverage matrix. |
||
|
|
78f97320de |
feat(goals): footer 'N pending' on cards + drop warning tone from top callout
- Pending pledges surface in the card footer as '· N pending' tacked on after the existing footer line (text-subdued). Quiet, semantic, doesn't compete with the status pill or the avatar. - The top-of-page 'You have pending pledges' callout was using the amber DS::Alert warning variant. Pending isn't a warning — it's a passive 'we're waiting on a sync' state. Switch to the info variant so the visual weight matches the meaning. |
||
|
|
66680877f4 |
fix(goals): swap pending-pledge avatar dot for inline clock glyph
The amber DS::Pill dot on the avatar collided visually with the amber 'Behind' status pill on the same card — two amber signals in the same eye-line. Move the indicator to a text-subdued clock icon between the goal name and the status pill: quieter, semantically clearer (clock = pending sync / time-bound), no tone collision with the status palette. Same accessibility hook (aria-label + title). |
||
|
|
25eb18d9af |
feat(goals): pending-pledge dot on card avatar
Goals with open + unexpired pledges now carry a small amber DS::Pill dot at the top-right of the avatar on the index card. Same primitive + position pattern as the beta gate dot on the sidebar nav, so the 'small marker' affordance reads consistently across the app. Pledges are preloaded via the existing .includes(:open_pledges, ...) on the index query, so the indicator is free at request time. |
||
|
|
82e3ba8ef7 |
fix(goals): keep archived cards out of the filter loop entirely
The earlier 'filter archived too' attempts kept toggling the archived section based on chip state, which produced more confusion than value (filter shows partial counts, archived hides on some chips, etc.). Step back: archived stays in its own collapsed-by-default section, always visible, never reacts to the chip / search filter. Render the cards with filterable: false so they don't add a filter target in the first place — no JS handling needed, and the active grid + chips behave exactly like they did before this whole thread. |
||
|
|
746d733599 | Merge branch 'feat/goals-v2-architecture' of https://github.com/we-promise/sure into feat/goals-v2-architecture | ||
|
|
adbef877a3 |
fix(goals): drop no-target-date goals from the on-track denominator
The KPI tile reads 'X of Y on track'. Y was every active goal minus reached + paused, which included open-ended goals (no target_date). But an open-ended goal has no required monthly pace to compare against — by definition it can be neither on track nor behind. Counting it in the denominator dragged the ratio down and never improved as the user kept saving (the fraction stays stuck because the open-ended goal is never a hit). Exclude :no_target_date from tracked_total. Numerator unchanged. The subline still surfaces 'N without a deadline' as informational so the user knows those goals exist. |
||
|
|
a3db348ce6 | Merge branch 'main' into feat/goals-v2-architecture | ||
|
|
dcb6f391e5 |
fix(goals): drop hover bg on card, use subtle shadow lift instead
The card was the only place in Sure setting hover:bg-surface-hover on a bg-container card; every other static card (settings, recurring transactions, ai_prompts, etc.) stays still. The hover bg (gray-100) landed almost exactly on top of the ring track (gray-200) and washed out the pill fill, fighting the very signals the card was trying to show. Swap to hover:shadow-sm — the lift matches the transaction-tabs precedent already in Sure, the bg stays white, and ring track + status pill keep their contrast. |
||
|
|
f0c9490c09 |
fix(goals): goal status pill uses DS::Pill outline (consolidate + survive hover bg)
The status pill on the goal card used a 10%-alpha fill (bg-warning/10, bg-green-500/10). On the card's hover state (bg-surface-hover) the fill blended into the new background and the pill lost its tint outline. Extend DS::Pill with a green tone and an optional icon: param (renders a Lucide icon in place of the dot) so the same primitive can carry both the beta marker and the goal status badges. Map Goals::StatusPillComponent to DS::Pill outline style — transparent fill + colored border + colored text + glyph — which is immune to any change in the surrounding card bg. One badge primitive, light-mode contrast already fixed (the color-mix 30% darkening on text), and the card hover state no longer washes out the status. |
||
|
|
bc9f13059a | Bump version by hand | ||
|
|
3bf36efc66 |
revert(goals): move .goal-avatar back into application.css
Sure's convention for one-off custom classes (.combobox, .scrollbar, .windows, .prose variants, .table-divider, .turbo-progress-bar) is to define them directly in application.css. Topical files (privacy-mode, date-picker-dark-mode, print-report) are reserved for coherent feature namespaces with multiple rules. A single .goal-avatar rule belongs with the other custom classes. |
||
|
|
612af6c14b |
fix(goals): apply CodeRabbit findings
- Switch the goal_accounts → accounts FK from on_delete: :cascade to
:restrict. `Goal#must_have_at_least_one_linked_account` is enforced
at write time; the cascade let a raw DELETE silently orphan a Goal
whose only link pointed at the deleted account. Normal Rails
Account#destroy still cleans up via `dependent: :destroy`, but the
restrict guarantees the DB rejects any path that bypasses the
association.
- projection_payload: required_monthly is now monthly_target_amount&.to_f
so open-ended (no-target-date) goals serialize required_monthly: null
instead of 0, matching the absence of a required pace.
- index page + sidebar nav-rail dot now read the Beta label via
t("shared.beta") (and a new shared.beta locale key) instead of the
hardcoded "Beta" literal.
- _status_callout uses the view-helper t(...) instead of I18n.t(...)
for the status label so it follows the same convention as the rest
of the goals views.
- goal_projection_chart: read the computed style before stamping
position: relative so a stylesheet-defined position (fixed/sticky/
absolute) isn't clobbered.
- preview-deploy: add `set -euo pipefail` around the wrangler
container lookup so a curl/jq failure fails the job instead of
producing an empty CONTAINER_ID and silently skipping cleanup.
|
||
|
|
3da89b30d3 | Merge branch 'feat/goals-v2-architecture' of https://github.com/we-promise/sure into feat/goals-v2-architecture |