* 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\`).
* 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>
* Extract hardcoded strings to i18n
Replace numerous hardcoded English strings with I18n lookups (t / I18n.t) across controllers, views, helpers, and components, and convert model validation error messages to symbol keys. Added multiple locale files under config/locales for models and views. This centralizes user-facing notices/alerts, UI text, import/validation messages, and prepares the app for localization and easier translation maintenance.
* Update en.yml
* Update preview-cleanup.yml
* Revert "Update preview-cleanup.yml"
This reverts commit 1ba6d3c34c.
* test: align i18n assertions with translated messages
* Standardize balance error key and tweak locales
Replace SophtronAccount's :requires_balance error key with :no_balance and update related locale strings for sophtron, plaid, and simplefin accounts to use the new key and clearer copy. Also switch the QIF upload redirect notice to use a relative translation key (t('.qif_uploaded')), remove an unused SSO providers help line, and fix a trailing-newline/whitespace issue in the subscriptions locale. These changes standardize validation keys and improve translation consistency and messaging.
---------
Co-authored-by: KiloClaw <kiloclaw@openclaw.ai>