Skip to content

Casadesign - DRAFT DO NOT MERGE#7051

Draft
giacoelho wants to merge 174 commits into
mainfrom
casadesign
Draft

Casadesign - DRAFT DO NOT MERGE#7051
giacoelho wants to merge 174 commits into
mainfrom
casadesign

Conversation

@giacoelho

Copy link
Copy Markdown
Collaborator

This is a work in progress. Will flag when it is ready to merge.

gia and others added 12 commits July 9, 2026 17:49
Introduce Tailwind v4 alongside the existing Bootstrap stack so the UI can be
modernized page-by-page without breaking current views.

- Add tailwindcss + @tailwindcss/cli (v4), build:tailwind[:dev] npm scripts,
  and a `tw` process in Procfile.dev. Output lands in app/assets/builds, which
  Sprockets already serves via the manifest's link_directory.
- Add app/assets/stylesheets/tailwind.css: CSS-first design system (Inter font
  + an indigo "brand" palette) — the visual direction for the redesign.
- New Tailwind-only layout casa_auth.html.erb (split brand panel + form),
  used by both the user and all-casa sign-in controllers.
- Redesign devise/sessions/new.html.erb in a clean, modern Stripe/Airbnb style
  and wire up remember-me properly.

Scoped to sign-in only; every other page keeps the Bootstrap `application`
layout untouched. First step of an incremental migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Approved font direction: swap the design-system typeface and the auth
layout's webfont link from Inter to Figtree (warmer humanist sans).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- design.md: living design-system doc (Tailwind v4 approach, Figtree type scale,
  color tokens, icon-tile pattern, component + empty-state specs, app-shell spec,
  migration checklist). Codifies the icon-tile treatment (icons on a soft colored
  background) for stat/status icons and reserves initial-avatars for people only.
- CLAUDE.md: point to design.md and record the casadesign commit/push-per-checkpoint
  workflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New Tailwind-only layout: left sidebar with Pundit-guarded nav mirroring the
existing routes, plus a top bar (notifications, profile menu, sign out). Coexists
with the Bootstrap `application` layout — redesigned authenticated pages opt in via
`layout "casa_app"`. Icons via Bootstrap Icons CDN + Figtree (per design.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Supervisors now land on a real dashboard (DashboardController#show renders it in the
casa_app shell) instead of the Bootstrap volunteers table:

- SupervisorDashboard service builds per-volunteer follow-up status + summary stats
  from the supervisor's assigned volunteers.
- View: KPI cards, a "Needs your attention" list (icon-tile treatment per design
  feedback — replaces the ringed avatars), and a roster with color-coded
  contact-status pills.
- Empty states handled: no volunteers -> welcome + CTAs; nobody behind -> all
  caught up. The full volunteer management table remains at /volunteers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Feedback from design review:
- Avatar initials strip honorific prefixes (Mrs./Mr./…) and use first + last name
  only, via a shared `avatar_initials` helper (removed the duplicate in the service).
- Top-bar profile menu is triggered by the avatar alone; the name lives in the
  dropdown, not the trigger.
- Page header is the page name ("Dashboard") + <title>, not a greeting.

Accessibility (WCAG AA targets):
- Skip-to-content link, landmark labels, aria-labels on icon-only controls,
  aria-current on the active nav item, table scope + sr-only caption, visible
  focus rings, contrast bumps (slate-400 -> 500/600), role=status/alert on flash.

Responsive:
- Sidebar becomes an off-canvas drawer below lg with an accessible toggle
  (button + aria-expanded, Escape/backdrop to close) and a responsive grid/padding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Names now render as first + last only (no Mr./Mrs./...) wherever a person is shown
in the redesign — the shell sidebar/profile menu and the supervisor dashboard
(names and aria-labels), plus the avatar initials.

- New `NamePresentation.strip_honorific` + a `display_person(user)` view helper.
- Presentation-only: the stored `display_name` is left untouched. (An attempted
  `User#display_name` override was reverted — a security spec requires display_name
  to round-trip raw input unchanged, e.g. dangerous strings.)
- Documented in design.md + CLAUDE.md so it's applied on every page going forward.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bottom-of-sidebar user block (avatar + name + role) duplicated the identity
already shown by the top-right avatar account menu, and was non-interactive. Drop
it; the top-right menu is the single source for identity + account actions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "CV" placeholder brand mark wasn't a value-add at 36px (the org name already
identifies the chapter) and using the real org logo there would add image/variant
infrastructure for little gain. Show the org name alone in the sidebar header; the
full logo stays for contexts with room (sign-in, court reports). design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the rest of the auth flow in line with the redesigned sign-in:
- Forgot password (passwords/new), reset password (passwords/edit), and
  accept-invitation (invitations/edit) rebuilt in Tailwind, matching the sign-in's
  inputs/buttons/error styling. CASA's email-or-phone reset is preserved.
- Users::PasswordsController uses `layout "casa_auth"`; the invitation-accept action
  renders in casa_auth (admin invite `new`/`create` untouched).
- casa_auth `<title>` is now driven by `content_for(:page_title)`; each page sets its
  own title (Sign in / Reset your password / Choose a new password / Accept your invitation).

Verified all three render 200 in the new layout (accept-invitation via a valid token).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply the honorific-free naming rule to the existing Bootstrap UI, not just the
redesign:
- 37 `.display_name` person-name sites across ~20 legacy views now render via the
  `formatted_name` helper (org names, form-value symbols, and the checkbox-id slug
  line were intentionally excluded).
- The 4 DataTables (volunteer, supervisor, case-contact, reimbursement) strip
  honorifics on their Ruby name outputs (display_name + supervisor_name); SQL
  select/order strings are untouched.
- Added a `formatted_name(name)` string helper alongside `display_person`, both
  backed by `NamePresentation`.

Verified /supervisors, /volunteers, /casa_cases render 200 with zero honorifics;
datatable classes load clean; standardrb + rendering green. Docs updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The header helper text becomes a short single sentence with no em-dash; the org
name already appears in the sidebar header, so it's no longer repeated here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added dependencies Touches dependency files javascript Touches JavaScript code ruby Touches Ruby code erb Touches ERB templates labels Jul 9, 2026
@giacoelho
giacoelho marked this pull request as draft July 9, 2026 23:01
gia and others added 3 commits July 10, 2026 15:34
DashboardController#show now renders the casadesign supervisor dashboard
(SupervisorDashboard + casa_app layout) for supervisors, but the request
spec still asserted the old redirect to the volunteers overview and was
failing on the branch. Assert the dashboard renders and lists an assigned
volunteer instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…app shell

First page-by-page migration off the legacy Bootstrap UI, following design.md.
The notifications page is linked straight from the new shell (top-bar bell +
account menu), so it was the most visible remaining Bootstrap seam.

- Render NotificationsController#index in the casa_app layout.
- Restyle the index, NotificationComponent, and patch-notes partial onto the
  design system: card list, leading icon tiles, unread dot, and a cold-start
  empty state. Switch Font Awesome (fas fa-*) to Bootstrap Icons (bi-*), which
  is the icon set actually loaded on Tailwind pages.
- Move the specs off Bootstrap-class selectors onto semantic data-* hooks
  (data-notification-list-item, data-read / data-unread-dot) without weakening
  behavior coverage. All notification/patch-note ordering, counts, and empty
  states remain asserted.

Verified: component, view, request (renders the full casa_app layout), and
system specs all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… backlog

- design.md: reframe as the permanent source of truth to refer to for all UI
  work. Add a "Design decisions (rationale)" log capturing the why behind the
  system (Tailwind-alongside-Bootstrap, layout opt-in, indigo/slate, Figtree,
  bi-* icons, icon-tile-vs-avatar, org-name-only sidebar, presentation-only
  honorific stripping, triage dashboards, empty-state taxonomy, a11y bar,
  build/output), a repeatable "Migrating a page" playbook, and a migration
  status that points at the backlog.
- design-todo.md: new prioritized, phased backlog (the "what's left") for the
  page-by-page migration off Bootstrap, plus cross-cutting infra and known
  pre-existing debt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the 🧪 Tests Tests label Jul 10, 2026
gia and others added 6 commits July 10, 2026 15:54
Second app-shell leaf page (linked from the account menu). Rebuilds the whole
edit-profile screen on the design system while preserving every behavior.

- Render UsersController in the casa_app layout.
- Rewrite edit.html.erb into cards: details, account, security, communication
  preferences, plus the volunteer languages table — using the documented input,
  button, and checkbox patterns.
- Replace the Bootstrap collapse accordions (Change Password / Change Email)
  with a small `disclosure` Stimulus controller (real <button> trigger, toggles
  a hidden panel, syncs aria-expanded) instead of relying on Bootstrap JS.
- Add a reusable Tailwind `shared/_form_errors` partial (the legacy
  `shared/_error_messages` stays for Bootstrap pages); it keeps the
  `@custom_error_header` hook so "... this password change ..." still renders.
- Preserve all field labels, ids, and JS-hook classes
  (toggle-email/sms-notifications, save-preference, password-new/-confirmation,
  submit-password, current_password_email, toggle-sms-notification-event) so
  require_communication_preference.js and password_confirmation.js keep working.

Verified: users request spec + the full edit system spec pass (79 examples);
erb-lint, standardrb, and StandardJS clean; JS bundle builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rofile done

Add the two components introduced by the edit-profile migration (the Tailwind
form-error summary and the disclosure/collapsible-panel pattern) to design.md,
and tick edit profile in both the design.md status and design-todo.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rd subtitle

- Add a role badge (current_role as a brand-tinted pill) to the casa_app account
  menu header, under name + email — the one place identity metadata lives, and a
  parity with the role line the legacy header used to show. Industry-standard
  placement for a multi-role internal tool (keeps the sidebar/top-bar chrome clean).
- Supervisor dashboard subtitle -> "Track volunteer progress at a glance."
- Document the role badge in design.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The person-badge icon added no signal, so remove it. Colour the pill by role
using the design system's non-semantic accent tints — Volunteer = sky (blue),
Supervisor = violet (purple), Casa Admin = amber — so roles read apart at a
glance; unknown roles fall back to slate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pt WCAG AA

- Drop the shouty `uppercase` transform from all design-system labels (account
  details, dashboard + languages table headers, patch-note category, auth "or"
  divider); render them sentence case. Sentence-case isolated copy ("My
  languages", "Learning hours this year").
- Bump muted label text from slate-400 (fails AA) to slate-500/600 for contrast.
- Redesign the form-error summary (shared/_form_errors): a role="alert" card with
  a leading icon and a tidy bulleted list instead of the plain block; keeps the
  heading text + #error_explanation so specs and behavior are unchanged.
- design.md: add Sentence case + Accessibility (WCAG 2.1 AA) as standing
  conventions, and fix the muted-text token. design-todo: track the app-wide
  interactive-label sentence-case pass and a slate-400 contrast audit.

Interactive button/field labels shared across pages (Update Profile, Change
Password, field labels) are spec-coupled across users/volunteers/all-casa-admin
and a shared example, so they're deferred to a holistic pass (tracked).

Verified: 109 examples across the touched pages pass; erb-lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per design feedback: one sentence, no bullets, dashboard-style icon tile.
- shared/_form_errors now renders "Unable to save: <messages via to_sentence>"
  inside a rose danger icon tile (bg-rose-100 text-rose-600, bi-exclamation-
  triangle) matching the dashboard, dropping the old heading + bulleted list.
- Update edit_spec assertions from the Rails "N errors prohibited this X from
  being saved:" text to "Unable to save".
- design.md: refresh the Form errors component note.

Verified: 79 users edit + request examples pass; erb-lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gia and others added 22 commits July 20, 2026 21:21
Full-suite run surfaced 15 reds; 13 fixed here:

- devise/passwords/new (8): stale devise-default labels — "Forgot your
  password?" -> "Forgot password?", "Send me reset password instructions" ->
  "Send reset instructions", "Change my password" -> "Update password",
  "Log In" -> "Sign in"; dropped the old Bootstrap "N errors prohibited" header
  assertion (the casa_auth page renders an inline error list; the error text
  itself is still asserted).
- banner_helper (2): #conditionally_add_hidden_class returns Tailwind "hidden"
  now, not Bootstrap "d-none".
- volunteers/edit impersonate (1): the banner shows formatted_name (honorific
  stripped, per the design rule), so gave the supervisor an explicit
  honorific-free display_name instead of the Faker default.
- casa_cases/index (1): retired the obsolete test of the removed Bootstrap
  "#ddmenu_my-cases" sidebar dropdown (the casa_app nav is flat links).
- learning_hours/volunteers/show (1): a coupled spec my round-2 sentence-case
  sweep missed — it still asserted "'s Learning Hours"; also dropped the stale
  "spec-locked" comment.

Left (pre-existing, unrelated to this work): truncated_text_component preview
spec (Lookbook preview JS) and timeout_warning (window.timeout isn't wired into
casa_app) — reported separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o 180)

The Bootstrap->casa_app migration dropped the inline script that set
window.timeout, so src/session_timeout_poller.js always fell back to its
`|| 180` default and ignored the user's configured timeout_in. Re-render it in
casa_app's head, before the deferred bundle, from @timeout_duration
(= current_user.timeout_in, already set by ApplicationController#set_timeout_duration).

Unit is minutes: the poller does `timeout * 60 * 1000` and the jest test sets
window.timeout in minutes, so render `.in_minutes.to_i` (180 for a 3h timeout),
`.to_json` for erb-safe script interpolation. Fixed timeout_warning_spec, which
asserted `.in_seconds` (10800) — that never matched the minutes the poller uses.

(all_casa_admin has the same latent pattern but resolves to the same global
config.timeout_in = 3.hours it already defaults to, so no behavior change there.)

erb_lint clean; spec green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The truncated_text preview spec failed because ViewComponent previews rendered
without the JS bundle, so the truncated-text Stimulus controller never connected
and "read more" did nothing. Add a component_preview layout that loads the
application bundle + tailwind, and point ViewComponent at it via
`config.view_component.previews.default_layout` (the 4.x key -- not the
`default_preview_layout` guess).

That surfaced a real component bug: TruncatedTextComponent + its controller used
Bootstrap `d-none` / `text-bold` / `d-inline`, which don't exist in the Tailwind
build -- so on the case-contact card (a live page) the [read more]/[hide] swap
was silently broken (both showed; the toggle no-op'd). Converted to `hidden` /
`font-bold`.

Spec green; jest 133 green (the read_more/case_button_states jest tests exercise
different code); builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record that the full suite was last at 3451 examples / 15 failures (all fixed,
verified in isolation) and note to re-run it end-to-end to confirm 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o ops

Move the platform activity charts off the public /health page and into the
authenticated all-CASA console, closing a cross-org exposure: the charts query
every chapter's data, but /health is public and unauthenticated.

- Extract HealthController's chart math into a MetricsReport query object that
  scopes to the whole platform by default, or to one CasaOrg when given
  (CaseContact via its dedicated `casa_org` scope; LoginActivity via the joined
  users.casa_org_id). This is the shared engine for the new Metrics/Analytics
  pages. Cross-org isolation verified (A=3, B=1, global=4).
- AllCasaAdmins::MetricsController#index renders the platform charts behind the
  existing all-CASA auth, nav'd as "Metrics" in the console sidebar.
- Shared metrics/_dashboard partial (range filter + 3 charts) so the upcoming
  per-chapter Analytics page reuses identical chart math.
- Rename HealthHelper -> MetricsHelper (its home is now Metrics/Analytics) and
  parameterize the range filter's base path.
- Slim /health to what it's for: a minimal, self-contained ops status page (HTML)
  plus the latest_deploy_time JSON. Drop the unused legacy JSON analytics
  endpoints and retire the now-dead metrics layout + JS bundle.

Charts render via the metric_* helpers + chart-hover controller, both already in
the application bundle, so no separate JS bundle is needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New AnalyticsController#index renders the same three activity charts as the
all-CASA Metrics console, but MetricsReport is scoped to current_organization
so each chapter sees only its own data. Gated by AnalyticsPolicy (admins and
supervisors, matching the Reports audience); nav'd as "Analytics" beside Reports
in the casa_app sidebar.

Specs: AnalyticsPolicy (admin/supervisor allowed, volunteer denied), a request
spec (roles + signed-out redirect + range preset), and a MetricsReport unit spec
asserting cross-org isolation (org A sees 3, org B sees 1, unscoped sees 4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three headline numbers above the charts, scoped to the signed-in chapter:
contacts this month (with a month-over-month delta badge), active volunteers,
and cases needing contact. Reuses AdminDashboard for the volunteer/needs-contact
figures (the app's canonical 14-day "no recent contact" definition, batched for
org scale) and adds contacts_this_month / contacts_previous_month to MetricsReport
for the delta. Metrics (all-CASA) stays charts-only; KPIs are chapter-actionable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The KPI cards had drifted from the documented "KPI stat card" pattern (missing
the semantic icon tile, wrong label/meta colors, an ad-hoc amber number and a
bespoke delta pill). Reconcile them to the exact token used by the dashboard
cards: icon tile -> text-3xl slate-900 number -> text-sm slate-500 label ->
text-xs slate-500 meta; "cases needing contact" now mirrors the dashboard's
amber attention treatment; the contact delta is a colored meta line (arrow +
signed number + "vs last month", never color-only).

Document the Metrics/Analytics feature in design.md (Charts now built in
MetricsHelper and rendered on Metrics/Analytics; KPI stat card variants; a
Migration status entry) and design-todo.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rack-test system specs covering the sidebar nav → page flow: an all-CASA admin
reaches Metrics and sees the platform charts; an admin reaches Analytics and sees
the KPI cards + charts; a supervisor can reach it; a volunteer sees no Analytics
nav link. (Charts are server-rendered SVG, so no JS driver is needed.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Found by the full-suite run: truncated_text_component_spec still asserted
`span.text-bold`, but the component was migrated to `span.font-bold` (the earlier
d-none->hidden / text-bold->font-bold change) without updating the spec. Update
both assertions to match the rendered class. Also record the full-suite result
in the backlog (3470 examples; the other 4 reds are headless-chrome flakes that
pass in isolation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3470 examples; 1 real stale-spec red fixed (truncated-text), the other 4 are
headless-chrome flakes that pass in isolation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…alytics pages

Converge the page vertical rhythm on index/list pages so the filter->table
and section gaps match instead of drifting page to page:

- volunteers: drop the reserved `min-h-[2.5rem]` band under the filter (a
  persistent ~68px empty gap above the table when nothing is selected). Put
  `mb-4` on the Manage button so the gap collapses to 16px, matching the
  cases index; the button still reveals on selection and pushes the table
  down only then.
- supervisors: filter `mb-5` -> `mb-4` (16px), matching the other rosters.
- analytics/metrics: range filter gains `mt-6` (it butted flush at 0px under
  the KPI row / subtitle); chart sections 2 & 3 `mt-5` -> `mt-6` so the whole
  dashboard reads as one uniform 24px column.
- design.md: document the page vertical rhythm and the anti-patterns.

Pixel-verified over CDP: filter->table = 16px on volunteers / cases /
supervisors / reimbursements; analytics above-filter = 24px, section gaps
= 24/24. Green: volunteers, supervisors, analytics, metrics system specs +
axe (volunteers/supervisors/reimbursements); standardrb + erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ged emoji

The case-contact card put a 36px medium-type icon in a full-height gutter
(`flex items-start`), indenting the title, secondary text, answers and notes
~48px right of the card edge. Move the icon into the header row inline with
the title (smaller 32px `h-8 w-8` tile) so the body spans the card's full
width, flush-left to the `p-5` edge — industry standard: a leading icon
adorns the header, it is not a body gutter.

Also drop the decorative transition-aged emoji from the case-number heading
(`display_case_number` returns the plain case number). It's data-as-emoji,
which design.md already bans; no other view renders it.

Pixel-verified (CDP): body indent 48px -> 0; icon 36 -> 32px; heading
"CINA-08-1001" (emoji gone). Green: case_contacts index system spec +
decorator spec (20 examples) + axe; standardrb + erb_lint clean.
design.md: documented the leading-icon card-header pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed title)

TruncatedTextComponent (the case-contact answer/notes detail lines) hard-coded
a `font-bold` label and set no font size, so it inherited 16px — making the
detail labels 16px/700, heavier AND bigger than the card's own 16px/600 title.
The detail shouted and the title whispered.

Align it to design.md's fact/detail-list convention: `text-sm` container, a
muted `font-medium text-slate-500` label, a regular `text-slate-800` value, and
a real `brand-600` `[read more]` link. Now the title (16px / 600 / slate-900)
is the sole anchor and every supporting/detail line is `text-sm`.

Pixel-verified (computed style): answer label 16px/700 -> 14px/500/slate-500;
value 16px -> 14px; read-more 16px/plain-text -> 14px/500/brand-600. Green:
truncated_text component + system specs + case_contacts index (19 examples);
erb_lint + standardrb clean. Updated the component label spec + design.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(kill read-more spam)

Every topic answer and the notes were each their own line-clamped
TruncatedTextComponent with a separate "[read more]", so reading a single
contact took several clicks (one per line). Replace them with ONE native
<details> "Show details" toggle per card that reveals the full answers + notes
at once as a dl (muted `text-xs font-semibold` dt / dark `whitespace-pre-line`
dd) -- matching the sibling new-design table's expandable detail panel.

Industry standard: detail is progressively disclosed with a single per-card
control, not fragmented behind N micro-toggles.

Specs: the two :js tests that pinned the old truncation are updated -- the
"contact notes" test now drives the single Show-details expand; the case-filter
test asserts on the always-visible section case numbers / an empty list rather
than collapsed notes. Full case_contacts index spec (16) + axe green; erb_lint
+ standardrb clean. design.md: documented the disclosure pattern and answered
the divider question.

Note: TruncatedTextComponent is now unused (left in place; safe to delete later).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…a-card) + sentence-case default contact types

The "Show details" panel boxed topic answers + notes in their own rounded-xl
border card *inside* the case-contact card -- a card-in-a-card, which isn't a
pattern in the app. Replace the box with a `border-t border-slate-100 pt-3`
divider on the <details> (matching metric_data_table's "View as table"
disclosure) and render the dl unboxed.

Separately, default contact types still displayed Title Case ("Fictive Kin",
"Social Services") though DEFAULT_CONTACT_TYPE_GROUPS is already sentence-cased:
generate_for_org! find_or_creates by name and never renames, so orgs seeded
before the constant change kept the old names. Add the missing after_party task
to rename them -- matching only case-variants of a shipped default so
org-renamed / custom names are left alone.

WCAG: the disclosure is a native <details>/<summary> (keyboard + screen-reader
expanded state), aria-hidden chevron, AA-contrast summary + labels; axe green.

Verified: case_contacts index spec (16) + axe green; erb_lint + standardrb
clean; ran the task on dev (renamed 3 groups + 39 types to sentence case).
design.md: documented the nested-card divider rule + constant-needs-migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Only conflict was app/views/case_contacts/_case_contact.html.erb: main (#7055)
added a reimbursement status badge to the Bootstrap card while casadesign had
rewritten the card in Tailwind. Kept the casadesign design and folded the new
feature in as a casadesign pill (amber = pending, emerald = complete) using the
merged reimbursement_status_text / _badge_type decorator methods, so main's new
specs (card shows "Reimbursement Pending"/"Complete") still pass.

Everything else auto-merged: dependency bumps (caxlsx 4.5, friendly_id 5.7,
strong_migrations 2.8, fontawesome 7.3, etc.) alongside casadesign's axe/Tailwind
deps; reimbursement controller + specs; decorator (medium_icon + reimbursement_*).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tack topic notes

Three UX fixes on the case-contact form:

- Relevant case(s): on edit (@case_contact.active?) the model requires
  draft_case_ids and the picker only ever offered the single case the contact
  belongs to, so the removable multiselect only let the user dismiss a required,
  fixed parent. Render it read-only (static chip(s) + hidden draft_case_ids);
  keep the editable TomSelect multiselect only for new / draft contacts.

- Topic answers: replace the 50/50 grid (topic | squeezed notes) with a stacked
  row — topic select on top, full-width notes textarea below, since the note is
  the substance and deserves the width.

- Delete: the standalone danger_outline button becomes a subtle ghost remove
  (design-system "recede for repeated-row actions"), keeping the visible "Delete"
  text + confirmDestroyAndRemove hook the spec drives.

All Stimulus/DOM hooks preserved. Green: contact_topic_answers / edit / new /
additional_expenses system specs (2 new_spec failures were Chrome-crash flakes,
pass in isolation); erb_lint clean. design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…emove topic"

Notes section discussion topics:
- Each topic was a rounded-lg bordered box inside the Notes card (a card-in-a-card).
  Replace with a border-t divider between topics (no box) — separates them without
  nesting cards.
- The "Delete" button read as "clear the field". Relabel to "Remove topic" (ghost,
  top-right of the topic) so it's clear it drops the whole entry; keep the
  confirmDestroyAndRemove hook. Updated the one spec that clicked "Delete".

Still open (recommended separately): the per-row topic dropdown disables already-used
topics, so it reads as redundant against the fixed 6-topic set — a fixed-topic
questionnaire (a textarea per topic) is the better navigation, but that's an
autosave / reject_if / JS / spec rewrite.

Green: contact_topic_answers system spec (11); erb_lint + standardrb clean. design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…re the dropdown)

Replace the "Add another discussion topic -> row with a dropdown (others disabled) ->
per-row Delete" nested-form with a fixed-topic checklist: every org topic is a
divider-separated row (checkbox + question) whose full-width notes textarea reveals on
check. No dropdown (it was redundant against a fixed topic set), no nested cards, and
unchecking is a clear "remove".

- New `contact-topics` Stimulus controller: create the ContactTopicAnswer on check
  (POST /contact_topic_answers, store the id), destroy on uncheck (confirm if notes
  exist); the 2s autosave then only *updates* the value via that id.
- CaseContact rejects id-less topic-answer attrs (reject_if: id blank) so a slow autosave
  can't create a duplicate; prepare_form no longer seeds a blank answer (it orphaned a
  nil-topic row). Checkbox label is associated via `for` (wrapping double-fires change ->
  double create).
- case-contact-form's topic-select/add-button code is now inert (already guarded by
  hasContactTopicSelectTarget); the reimbursement toggle is unaffected.

Specs: rewrote the topic-answer system spec + shared helper to the checklist; updated the
new/edit specs (no dropdown / add button) and the form request spec (the wizard update now
only *updates* topic answers -- creation is the /contact_topic_answers endpoint, already
spec'd). Green: contact_topic_answers / new / edit / additional_expenses system specs (54),
form request spec (53), case_contact model + case_contacts request (94); erb_lint +
standardrb + standardjs clean. design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…de the collapsible form)

The reimbursement section's autosave status line (an `invisible` <small>) sat at the card's
top level, so when reimbursement isn't requested (form collapsed) it reserved a blank line +
space-y gap -- ~40px of dead space that made the empty card look over-padded next to the other
cards (all the same p-6). Move the status line inside the collapsible reimbursement form so the
empty state is just heading + checkbox (measured card height 158 -> 138px). Autosave feedback
still shows once the form is expanded.

design.md: documented the collapsible-section padding rule, and that volunteer_address is a
single free-text string (Address#content, shared with the volunteer profile + reimbursement
exports) -- so structured address fields are a cross-cutting Address-model change, not a tweak.

Verified: additional_expenses + edit system specs (16) green; erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…/ state / zip)

Replace the single free-text address field with structured parts on the volunteer profile edit
(users/edit) and the admin's volunteer edit (volunteers/edit). The Address model keeps `content`
as the canonical composed one-line value every reader already uses (reimbursement table, mileage
CSV export, case-contact prefill): compose_content syncs it from the parts on save and only runs
when a structured field is present, so legacy content-only rows and the factory are untouched.

New nullable columns (Strong Migrations safe); an after_party task seeds line_1 from legacy
content (a line_1-only address composes back to exactly line_1, so readers are unaffected).
Params permit the new attrs alongside :content for backward compatibility.

This is the canonical-Address (profile) layer. The reimbursement-card address is a separate
follow-up: it's a per-contact snapshot string with a two-way profile sync and an autosave
contract, so structuring it safely is its own change.

Verified: address_spec (compose/structured), users request spec (incl. content backward-compat),
volunteers/edit address system spec -- all green; standardrb + erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the db Touches database schema or migrations label Jul 21, 2026
gia and others added 7 commits July 21, 2026 19:25
…ne 2 / city / state / zip)

The reimbursement card's single free-text "Mailing address" becomes structured fields, matching
the volunteer profile. They are five virtual attrs on CaseContact that compose into the existing
volunteer_address snapshot string (Address.compose, shared with Address#content), so storage, the
reimbursement validation, exports, and readers are unchanged. compose_volunteer_address is skipped
when all parts are nil, preserving the legacy single-string path (request specs, factory) and
yielding "" on a blank submit so the reimbursement-wanted validation still fires.

Fields prefill from the volunteer's structured Address (decorator#volunteer_address_parts); each
is a volunteerAddress Stimulus target so clearMileage empties all five on uncheck; the ambiguous
multi-volunteer state disables them as before. On submit, update_volunteer_address syncs the
structured parts back to the volunteer's profile Address (legacy content path retained).

Specs target the line-1 field (a line-1-only compose == the old single value, so the
"123 Example St" expectations hold).

Verified: form request + case_contact/address/decorator model specs (163) green; new
(reimbursement section), edit, additional_expenses system specs (25) green; standardrb, standardjs,
erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + clearer contact-type recency

On edit, the required case moves from a read-only chip in a form cell to a badge beside the
"Details" heading (it's fixed context, not an editable field), and the now-lone date field is
left-aligned instead of sitting in a half-empty 2-col grid. New/draft contacts keep the TomSelect
multiselect + date grid. The #draft-case-id-selector id and hidden draft_case_ids are preserved
for the JS/spec contract.

The unlabeled "never" / "3 days ago" next to each contact type was the last time that type was
logged for this case — confusing as a bare word. It now reads "Last logged 3 days ago" and is
omitted entirely when never logged. ContactTypeDecorator#last_logged_hint_with_cases (nil for
never) backs the form; #last_time_used_with_cases is unchanged for the multiselect subtext.

Verified: contact_type_decorator (8), new (28) + edit (8) system specs green; standardrb + erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… not window.confirm

Unchecking a discussion topic that still has notes triggered a browser-default confirm
("Remove this topic and its notes?"). It now opens the native <dialog> confirmation, matching
the shipped court-order remove pattern: contact-topics keeps the topic checked, opens its dialog
target via showModal(), and removes the pending topic on confirm (Cancel / Escape / backdrop keep
it). Confirm/Cancel use the Dialog:: Header/Body/Footer suite + button_classes.

Verified: contact_topic_answers system spec (11, incl. the confirm path + a new cancel path)
green; standardjs + erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e card padding

- Deleting a SAVED additional expense now opens the native <dialog> confirmation (court-order
  pattern: casa-nested-form opens its dialog target via showModal and deletes on confirm); brand-new
  unsaved rows still remove without a prompt. Removed the dead window.confirm confirmDestroyAndRemove.
- Additional-expense rows are border-t dividers instead of grey bg-slate-50 cards, reusing the
  standard input style so they match the rest of the form (all .nested-form-wrapper / data hooks kept).
- Autosave status lines toggle display (hidden/block) instead of visibility (invisible), so an idle
  Notes/Reimbursement card reserves no trailing line and its bottom padding matches the Details card.
  (autosave is used only on this form, so the change is scoped.)

Verified: additional_expenses (9, incl. confirm + new cancel), contact_topic_answers (11, autosave
"Saved!"), edit "autosaves notes" — green; standardjs + erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e fields + trash Delete)

The divider treatment read poorly and left the Delete button flush against the next line. Replace
it with the design-system nested-row pattern from casa_cases/_court_order_fields: a bordered card
(rounded-lg border-slate-200 p-3, no grey fill) that lays amount / details / a trash "Delete"
inline (sm:flex-row sm:items-start), with rows spaced by space-y-3. Fields use aria-label +
placeholder instead of stacked labels, matching the court-order row. All JS hooks (.nested-form-wrapper,
.expense-*-input, .remove-expense-button, data-*, hidden id/_destroy) are preserved.

Verified: additional_expenses system spec (9) green; erb_lint clean. design.md note corrected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ount/description/Delete

Per review: go back to a divider between expenses (border-t border-slate-200 + symmetric py-4, so
Delete no longer sits flush against the next divider), restore the visible field labels, and stack
the fields vertically — amount, then description (required), then a right-aligned Delete below —
instead of the inline bordered-card layout. All JS hooks (.nested-form-wrapper, .expense-*-input,
.remove-expense-button, data-*, hidden id/_destroy) preserved.

Verified: additional_expenses system spec (9) green; erb_lint clean. design.md note updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t a ghost-button row

Replace the full-width danger_outline "Delete" ghost button (its own row below the fields, which
pushed content down and added whitespace) with a compact trash icon button pinned to the top-right
of each expense row (flex items-start, h-8 w-8, rose hover). Fields stay stacked with visible
labels in the left column. Specs click the .remove-expense-button hook (the button is icon-only
now). The saved-expense confirm dialog is unchanged.

Verified: expense remove / cancel / add system specs green (in isolation; the full-file run has the
known Chrome-under-load flake on an unrelated add example); erb_lint clean. design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

db Touches database schema or migrations dependencies Touches dependency files erb Touches ERB templates javascript Touches JavaScript code pundit Touches Pundit authorization policies ruby Touches Ruby code 🧪 Tests Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants