Commit Graph

15 Commits

Author SHA1 Message Date
Guillem Arias Fauste
cc8e2abf18 fix(design-system): DS::Menu add :icon_sm variant for dense action lists (#1930)
PR #1840 bumped DS::Button icon-only `:md` size from `w-9 h-9` (36×36)
to `w-11 h-11` (44×44) for WCAG 2.5.5 enhanced touch target. DS::Menu's
`:icon` variant uses DS::Button at the default `:md` size, so every
row-level "..." action-list trigger grew from 36×36 to 44×44.

For dense lists where each row has a trigger — most visibly the
transaction category dropdown (`category/dropdowns/_row.html.erb`) —
the per-row height bump (+8px) compounds: a 5-category panel that
used to fit in ~220px now wants ~260px, the badges look smaller
relative to the row chrome, and the overall density that made the
dropdown scannable regresses visibly.

Add an `:icon_sm` variant that renders the trigger as DS::Button at
`size: :sm` (32×32). Meets WCAG 2.5.8 AA (24×24) — appropriate for
compact in-row triggers where 44×44 isn't required. Standalone
toolbar / row-action `...` triggers should keep `:icon` for AAA.

Migrate `category/dropdowns/_row.html.erb` to `:icon_sm` to restore
the pre-#1840 dropdown density.
2026-05-23 09:18:16 +02:00
Guillem Arias Fauste
09058b0cc6 feat(design-system): extend DS::Pill with badge mode + semantic tones (#1751 PR A) (#1902)
* feat(design-system): extend DS::Pill with badge mode + semantic tones (#1751)

Adds two extensions to the existing `DS::Pill` (originally landed as a
stage marker primitive in #1829) so it can also serve as the shared
status / category badge across the app — the use case tracked by #1751.

**Badge mode (`marker: false`)**

The original `DS::Pill` was intentionally sub-12px (text-[10px] /
text-[11px]) + uppercase + tracking-wide so it reads as a marker
(`Beta`, `Canary`, `NEW`), not a label. That shape is wrong for
status badges where the surrounding context is regular UI copy and the
pill needs to feel like a chip (`Pending`, `Active`, `Past due`,
`Failed`).

The new `marker: false` flag drops the uppercase + arbitrary
sub-12px text and snaps the chrome to the DS text scale:

- `marker: false, size: :sm` → `text-xs` (12px), normal case
- `marker: false, size: :md` → `text-sm` (14px), normal case
- `marker: true` (default) → existing #1829 behavior, unchanged

**Semantic tone aliases**

Status badges read more naturally with semantic tone names than with
the underlying palette colors:

| Alias | Resolves to |
|---|---|
| `:success` | `:green` |
| `:warning` | `:amber` |
| `:error` / `:destructive` | `:red` (new tone, added here) |
| `:info` | `:indigo` |
| `:neutral` | `:gray` |

Visual-name tones (`:violet`, `:indigo`, `:fuchsia`, `:amber`,
`:green`, `:gray`, `:red`) still work as before — semantic aliases
resolve through `SEMANTIC_TONE_ALIASES` at component init time, so
the callsite can pick whichever name reads better. Unknown tones
still fall back to `:violet` (existing behavior).

**Red palette**

Adds the `:red` tone (palette already present in
`design/tokens/sure.tokens.json` — `red-50/100/200/500/700/tint-10`).
Needed for `:error` / `:destructive` status badges.

**Icon slot**

Adds an `icon:` option (already documented in the component's
doc-comment as planned). When set, the Lucide glyph replaces the
colored dot inside the pill — useful for status badges that read
better with a glyph (`circle-check`, `triangle-alert`,
`loader`, etc.) than the generic dot.

**Scope**

API + tests + Lookbook preview only. No callsite migrations in this
PR — that's the next slice of #1751, done as separate per-bucket PRs
(transaction badges, provider badges, misc) to keep diffs small.

DS::Pill currently has no in-app callsites (#1829 shipped the
primitive ahead of consumers), so this is a pure-additive change.
Existing API is fully backwards-compatible — `marker:` defaults to
`true`, so without that flag the pill renders exactly as it does
today.

* fix(test): use assert_no_selector for dot-suppression assertion

`refute_selector ..., count: 1` only fails when there are exactly 1
matches — it would silently pass for 0 OR 2+. The intent is "no dots
should render when an icon is set"; `assert_no_selector` strictly
asserts zero matches.

Flagged by coderabbit on #1902.
2026-05-22 02:16:33 +02:00
Guillem Arias Fauste
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>
2026-05-20 18:30:25 +02:00
Guillem Arias Fauste
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>
2026-05-20 18:17:51 +02:00
Guillem Arias Fauste
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>
2026-05-19 14:41:02 +02:00
Guillem Arias Fauste
5249842c76 feat: beta features toggle + Beta pill primitive (#1829)
* feat: beta features toggle + Beta pill primitive

Adds the infrastructure for self-service beta opt-in. No call sites yet:
this PR is meant to land first so feature PRs (Goals, etc.) can ship
behind the gate incrementally.

User opts in via a single toggle at the bottom of Settings → Preferences.
The flag persists in the existing `users.preferences` JSONB column under
`beta_features_enabled` — same shape as `dashboard_two_column` and
`show_split_grouped`, so no migration is needed.

Controllers gate a beta feature by adding `before_action
:require_beta_features!` from the new `BetaGateable` concern (included in
ApplicationController). Views use the `beta_features_enabled?` helper to
hide / show nav items, banners, etc. Logged-out callers always return
false.

Ships `DS::BetaPill`, a small inline marker for tagging features as
Beta / Canary in nav, headers, and lists. Five tones (violet by default,
indigo, fuchsia, amber, gray) map to existing Sure color tokens — no raw
hex. Three styles (soft / filled / outline) and two sizes (sm / md) cover
the surfaces in the design handoff. The `dot_only:` mode renders just
the colored dot for use on a collapsed sidebar.

* review: rename to DS::Pill, fix CR/Codex nits, add tests

CodeRabbit + Codex review feedback:

- Rename DS::BetaPill → DS::Pill. The component was already generic in
  shape (tones, styles, sizes); the name was misleading scope. "Beta"
  becomes the default label (still i18n-driven). Goals' StatusPill can
  later refactor onto this primitive without a third pill.
- Localize the default pill label via i18n (`ds.pill.default_label`)
  instead of hard-coding English.
- Add role="img" to the dot-only span so the aria-label is consistently
  exposed to assistive tech.
- Wrap the Preferences toggle row in <label for="…"> so the title and
  description become an honest click target for the toggle (matches the
  cursor-pointer affordance).
- Drop arbitrary Tailwind values (py-[3px], gap-[5px], tracking-[…]) in
  favor of scale tokens. text-[10/11px] stays because the pill is
  intentionally sub-12px (Sure's smallest scale token is text-xs / 12px)
  to read as a marker, not a label.
- Add User#beta_features_enabled? predicate tests covering default-off,
  explicit-true, and non-boolean truthy values.

Won't fix:
- Palette refs (`--color-violet-*` etc.). Sure has no semantic Beta/
  Canary tokens; introducing them in this PR would be a design-system
  change beyond the scope. The component centralizes palette use in one
  `palette` method, matching the existing pattern in
  Goals::StatusPillComponent.

* review: consistent title fallback in full-pill branch

* docs: how to gate a feature behind the beta toggle

* docs: unwrap doc lines to match existing style

* chore(preview): run Cloudflare PR previews on basic instances (#1831)

* fix(preview): use Rails health endpoint for container ping (#1823)

* fix(preview): use Rails health endpoint for container ping

* fix(preview): point container ping to localhost/up

---------

Co-authored-by: Sure Admin (bot) <sure-admin@splashblot.com>
2026-05-18 20:07:55 +02:00
Guillem Arias Fauste
57d71cd55e refactor(design-system): extend DS::Alert and migrate 9 inline alert blocks (#1731)
* feat(design-system): add info semantic color token

Mirrors success/warning/destructive: --color-info maps to blue-600 in
light mode, blue-500 in dark mode. Unblocks the DS::Alert info variant
from carrying a raw 'blue-600' literal in icon_color and lets surface
tokens use bg-info/N alpha modifiers like the rest of the system.

Refs #1715

* refactor(design-system): adopt semantic tokens and add body slot in DS::Alert

Replaces the bg-{blue,green,yellow,red}-50 / text-{...}-700 / border-{...}-200
palette block in DS::Alert with semantic alpha-modifier surfaces
(bg-{info,success,warning,destructive}/10 + matching /20 borders).
Drops the 'blue-600' literal that icon_color was returning for the
info variant; helpers#icon now accepts color: :info backed by the
new --color-info token.

Adds an optional title: kwarg and an opt-in block-content slot so
rich alerts (title + paragraph, lists, embedded actions) can render
without callers reaching for a hand-rolled flex layout. The existing
message: API stays backward-compatible — nothing in the codebase that
already calls DS::Alert.new(message: ..., variant: ...) needs to change.

Lookbook gains with_title and with_body_slot examples covering the
new shapes.

Refs #1715

* refactor(views): migrate api_keys, hostings, lunchflow alerts to DS::Alert

Cleans up nine bespoke alert blocks that hand-rolled the same
flex + icon + bordered-surface shape DS::Alert already provides:

- settings/api_keys/{new,created,created.turbo_stream}.html.erb — three
  near-identical 'Security Warning' / 'Important Security Note' boxes
  using the broken bg-warning-50 / text-warning-700 raw-palette pair.
- settings/hostings/{_alpha_vantage,_eodhd,_yahoo_finance,_twelve_data,_provider_selection}_settings.html.erb —
  five amber-50 / amber-200 warning boxes covering rate-limit notes,
  health-check failure messaging, and the env-configured override
  banner. The twelve_data plan-restriction block keeps its bullet
  list and pricing link inside the new DS::Alert body slot.
- lunchflow_items/{_api_error,_setup_required}.html.erb — two modal
  alert headers whose flex+icon scaffolding now collapses onto
  DS::Alert. The surrounding bg-surface 'Common issues' / 'Setup
  steps' info cards stay as-is; this PR only touches the alert
  shape itself.

No functional or behavioural changes. Locale keys preserved.
amber-* palette uses on the alerts disappear; remaining bg-amber-*
hits in the codebase live outside the alert pattern and stay for
follow-up sub-PRs of #1715.

Refs #1715
2026-05-10 17:14:06 +02:00
Guillem Arias Fauste
0fe1e06645 refactor(design-system): migrate fg-* utilities to text-* and remove namespace (#1626)
* refactor(design-system): migrate fg-* utilities to text-* and remove namespace

The design system carried two parallel namespaces for foreground colors:
text-* (canonical, ~2,000 uses) and fg-* (32 uses). Most fg-* tokens
were 1:1 duplicates of a text-* counterpart. fg-gray was nearly
identical to text-secondary, with a one-step shade difference in dark
mode.

This PR migrates all 32 usages to their text-* equivalents and removes
the fg-* block from the design tokens. Closes #1606.

Mapping:
- fg-inverse  -> text-inverse  (20 usages, identical light/dark values)
- fg-gray     -> text-secondary (7 usages; light values match, dark is
                                 one step lighter: gray-300 vs gray-400)
- fg-primary  -> text-primary  (3 usages, identical values)
- fg-subdued  -> text-subdued  (2 usages, identical values)

The four other fg-* tokens (fg-contrast, fg-primary-variant,
fg-secondary, fg-secondary-variant) had zero usages despite being
defined; they are removed without replacement.

JSON / build:
- design/tokens/sure.tokens.json: $version 1.0.0 -> 2.0.0 (breaking
  schema change per the policy added in #1620). 8 fg-* token
  definitions removed.
- button-bg-ghost-hover's dark value still references "fg-inverse"
  internally; rewritten to "bg-gray-800 text-inverse" so the cleanup
  doesn't break that utility.
- _generated.css regenerated. 42 utility blocks now (was 50).

Lookbook tokens preview:
- The Text & foregrounds section dropped its split between text-*
  (canonical) and fg-* (legacy). Now a single section listing the
  five text-* utilities. The "(legacy)" framing is gone since there's
  no legacy left.

README:
- design/tokens/README.md's button-bg-ghost-hover edge-case example
  updated to reflect the new "bg-gray-800 text-inverse" dark value.

Visual review needed in dark mode:
- Anywhere icons use the application_helper#icon helper with
  color: "default" (most icons in the app). The default class moved
  from fg-gray (gray-400 dark) to text-secondary (gray-300 dark), so
  default-color icons render slightly lighter in dark mode.
- DS::Buttonish icons in secondary buttons (same shade shift).
- DS::Link icons (same).
- Time series chart axes (same).
- All tooltips, account add flow, settings hostings buttons,
  invitations, AI consent, family export, danger-zone buttons --
  these used fg-inverse, which is identical to text-inverse, so no
  visual change expected.

* fix(design-system): use inverse pair on tooltips for readable dark mode

* fix(lookbook): use semantic tokens in menu preview header text

* fix(lookbook): set text-primary on layout body so previews inherit theme

* fix(design-system): keep shadows dark-toned in dark mode

Inverting shadows to white|8% on dark surfaces produces a halo
effect rather than an elevation cue, and stacks redundantly with
the alpha-white 1px ring already in shadow-border-*.

Switch dark-mode shadows to black at progressively higher alpha
(25%/30%/35%/40%/50% for xs..xl) so they read as actual cast
shadows on near-black surfaces. Surface-tint differences and the
existing alpha-white border ring continue to handle elevation
hierarchy and edge definition.

Approach matches Material 3, Apple HIG, IBM Carbon, Refactoring UI,
and the dark-mode shadows used in Linear/Vercel/Stripe.

* fix(design-system): set text-primary on DS::Dialog element

Browser UA stylesheets apply color: black directly to <dialog>,
which overrides ancestor inheritance even when a body or html
ancestor sets a theme-aware color. Unstyled child content then
renders black regardless of theme.

Setting text-primary on the dialog element itself defeats the UA
override and lets descendants inherit the semantic token.

* fix(lookbook): use shadow css vars in effects preview so dark theme renders

* Revert "fix(design-system): keep shadows dark-toned in dark mode"

This reverts commit 3e9d76ed0b.

* fix(design-system): use opacity-70 instead of text-inverse/70 in value tooltip

The custom @utility text-inverse expands to @apply text-white and
isn't modifier-aware, so text-inverse/70 produced no CSS at all and
the muted labels fell through to inherited color (invisible on the
white pill in dark mode).

Replace with text-inverse + opacity-70. Same visual effect, works
with the existing utility definition.
2026-05-04 00:50:52 +02:00
Guillem Arias Fauste
a7e964f8be feat(design-system): live tokens reference page in Lookbook (#1618)
* feat(design-system): live tokens reference page in Lookbook

Adds `DesignTokensPreview` at `/design-system/inspect/design_tokens/*`,
split into seven sub-pages (typography, palette, surfaces, text,
borders, controls, effects). Each reads `design/tokens/sure.tokens.json`
at request time and renders the corresponding slice with values
pre-resolved to literal hex / rgba in Ruby — Tailwind doesn't need to
keep every CSS variable alive for the swatches to render.

Also drops the `@source not "../../../design/tokens"` directive added
in #1604. Excluding the JSON tree-shook ten or so design system
utilities that aren't yet used in app views (`shadow-border-md/sm/xl`,
`button-bg-ghost-hover`, etc.). The preview references each utility
through dynamic ERB, which Tailwind's scanner can't follow, so those
swatches were rendering blank. Letting Tailwind scan the JSON keeps
every declared utility available, which matches the intent of a design
system. Compiled CSS grows by about 3 KB.

Stacked previously on the `refactor/design-system-tokens` branch behind
#1604; rebased onto `main` once that landed.

* style(design-system): apply rubocop indented_internal_methods to preview

CI lint flagged the private helpers in DesignTokensPreview because the
project's RuboCop config uses `indented_internal_methods` style (methods
after `private`/`protected` get an extra 2-space indent). Auto-fixed
with `bin/rubocop -A`.

* fix(design-system): pre-resolve utility token values for the preview

CodeRabbit caught: collect_utilities was passing raw `{ref}` strings
(e.g. `{color.gray.50}`) as light_value/dark_value, while the rest of
the class pre-resolves to literal hex / rgba. The four templates that
display them (surfaces, text, borders, controls) showed the unresolved
template strings to users.

Adds `light_resolved` / `dark_resolved` fields to each utility entry,
populated via the same `resolve_template` helper the other collectors
use. Templates display `:light_resolved || :light_value` so plain class
strings (e.g. `border-tertiary`, `bg-gray-800 fg-inverse`) and compose
cases still fall through correctly.
2026-05-01 16:06:25 +02:00
Juan José Mata
97a8cb9407 Fix /design-system for tabs 2025-11-25 14:11:50 +00:00
Juan José Mata
5706280dd7 More rebranding changes (#159)
* Replace Maybe for Sure in select code areas

* Make sure passwords are consistent

* Remove (admin|member) from demo data first name

* Database and schema names finally to `sure`

* Fix broken test

* Another (benchmarking) database name to `sure_*`

* More rebranding to Sure

* Missed this Maybe mention in the same page

* Random nitpicks and more Maybes

* Demo data accounts and more Maybes

* Test data account updates

* Impersonation test accounts

* Consistency with `compose.example.yml`
2025-09-24 00:19:51 +02:00
Zach Gollwitzer
e8eb32d2ae Start and end balance breakdown in activity view (#2466)
* Initial data objects

* Remove trend calculator

* Fill in balance reconciliation for entry group

* Initial tooltip component

* Balance trends in activity view

* Lint fixes

* trade partial alignment fix

* Tweaks to balance calculation to acknowledge holdings value better

* More lint fixes

* Bump brakeman dep

* Test fixes

* Remove unused class
2025-07-18 17:56:25 -04:00
Zach Gollwitzer
ab6fdbbb68 Component namespacing (#2463)
* [claudesquad] update from 'component-namespacing' on 18 Jul 25 07:23 EDT

* [claudesquad] update from 'component-namespacing' on 18 Jul 25 07:30 EDT

* Update stimulus controller references to use namespace

* Fix remaining tests
2025-07-18 08:30:00 -04:00
Zach Gollwitzer
662f2c04ce Multi-step account forms + clearer balance editing (#2427)
* Initial multi-step property form

* Improve form structure, add optional tooltip help icons to form fields

* Add basic inline alert component

* Clean up and improve property form lifecycle

* Implement Account status concept

* Lint fixes

* Remove whitespace

* Balance editing, scope updates for account

* Passing tests

* Fix brakeman warning

* Remove stale columns

* data constraint tweaks

* Redundant property
2025-07-03 09:33:07 -04:00
Zach Gollwitzer
90a9546f32 Pre-launch design sync with Figma spec (#2154)
* Add lookbook + viewcomponent, organize design system file

* Build menu component

* Button updates

* More button fixes

* Replace all menus with new ViewComponent

* Checkpoint: fix tests, all buttons and menus converted

* Split into Link and Button components for clarity

* Button cleanup

* Simplify custom confirmation configuration in views

* Finalize button, link component API

* Add toggle field to custom form builder + Component

* Basic tabs component

* Custom tabs, convert all menu / tab instances in app

* Gem updates

* Centralized icon helper

* Update all icon usage to central helper

* Lint fixes

* Centralize all disclosure instances

* Dialog replacements

* Consolidation of all dialog styles

* Test fixes

* Fix app layout issues, move to component with slots

* Layout simplification

* Flakey test fix

* Fix dashboard mobile issues

* Finalize homepage

* Lint fixes

* Fix shadows and borders in dark mode

* Fix tests

* Remove stale class

* Fix filled icon logic

* Move transparent? to public interface
2025-04-30 18:14:22 -04:00