Skip to content

Casadesign - DRAFT DO NOT MERGE#7051

Draft
giacoelho wants to merge 57 commits into
mainfrom
casadesign
Draft

Casadesign - DRAFT DO NOT MERGE#7051
giacoelho wants to merge 57 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 30 commits July 10, 2026 19:45
…ataTables)

Reverses the "theme jQuery DataTables" approach — theming couldn't match the
dashboard tables or meet WCAG (its generated chrome fights the design system).
The cases index is now a hand-built Tailwind table matching the dashboard, with
server-side filtering + Pagy pagination.

- Controller: server-side filters (status [default active], assignment,
  transition-aged, prefix) + Pagy; drop the client-side DataTables path.
- View: bespoke table (readable headers, empty-state row, card + inset), a clear
  filter bar of selects that submit on change (new auto-submit Stimulus
  controller), and a reusable shared/_pagination partial (Pagy -> "Showing X-Y of
  Z" + page controls; WCAG nav/aria-current/rel). Consistent action buttons.
- Remove the now-dead DataTables Tailwind skin and the dropdown controller (its
  popover filters are replaced by selects).
- Rewrite the coupled specs (index + additional_index) for server-side filtering.
- design.md / design-todo: reverse the decision; document bespoke tables +
  pagination as the pattern.

Dropped for v1 (follow-ups): column-visibility picker, sortable headers. Action /
header labels stay Title Case (spec-coupled).

Verified: cases index / additional_index / request specs, dashboard/show, and the
new-case + case-group nav flows all pass; erb-lint, standardrb, StandardJS clean.

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

Review feedback on the cases index:
- Filter selects: `appearance-none` + a positioned `bi-chevron-down` so the
  dropdown chevron has proper right padding (was the cramped native arrow).
- Row action: the stranded far-right "Edit" text link is now a compact
  right-aligned pencil icon button (aria-labelled), matching the dashboard roster.
- Transition Aged Youth: drop the whimsical 🦋/🐛 emoji (butterfly = aged,
  caterpillar = not) for a clean indicator — a violet "Yes" pill / muted "No".
  Scoped to the new UI; the shared decorator keeps the emoji on unmigrated legacy
  pages. Documented in design.md.
- Blank hearing-type / judge cells show a muted em-dash instead of empty.

Verified: cases index / additional_index / dashboard-show specs pass; erb-lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An icon-only pencil wasn't clear enough. Restore a visible "Edit" label as a
compact bordered button; keep the per-row aria-label ("Edit <case number>") so
the accessible name stays unique and descriptive (WCAG 2.4.4 / 2.5.3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cases index carried "Hearing Type" and "Judge" columns, but migration
20230729213608 (2023) removed hearing_type_id/judge_id from casa_cases and moved
that data onto court dates — so those two columns had rendered blank for every
case since. Drop them and show a single, meaningful "Next court date" instead:

- CasaCaseDecorator#formatted_next_court_date picks the earliest upcoming court
  date from the case's (preloaded) court_dates and formats it (:full); blanks
  render a muted em-dash.
- Controller preloads :court_dates (dropping the dead :hearing_type/:judge
  eager-load) — instrumented the request to confirm a single court_dates query
  for the whole page (no N+1).
- Sentence-cased the (now index-owned) column headers.
- Decorator unit specs cover upcoming/none/past-only; additional_index_spec updated.

design.md + design-todo.md: record the finding, add a stakeholder question (is
next court date the right roster column, or the upcoming hearing's type/judge?),
and flag the vestigial CasaCase#hearing_type/#judge associations for eventual
removal (still referenced by supervisors/index + legacy pages).

Verified: casa_cases index / additional_index / request + decorator specs
(97 examples) pass; erb-lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Edit action had been rebuilt as a *secondary* button (bordered white +
shadow) — too much emphasis for a control that repeats on every row and competes
with the brand-coloured case-number link. Make it tertiary/ghost: no border,
fill, or shadow; neutral slate-600 ink (>= AA under visible text, unlike the
slate-400 icon-only variant); leading pencil icon; subtle slate-100 hover wash.
Give the trailing actions cell an extra end gutter (pr-6) so the control clears
the card edge without skewing the button's own padding.

Documented the Tertiary (ghost) variant in design.md.

Verified: casa_cases index / additional_index specs pass; erb-lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three peer buttons (one primary + two secondary) diluted the primary CTA and
crowded the header. Keep "New Case" as the standalone primary and move the two
lower-frequency, admin/supervisor-only actions — Case Groups and New Bulk Court
Date — into a "More" overflow menu.

The menu is a native <details>/<summary> disclosure (matching the header account
menu), enhanced by a new `dropdown` Stimulus controller that closes it on
outside-click and Escape (returning focus to the summary) and degrades to the
native toggle without JS. Retrofit the account menu onto the same controller so
the shell is consistent at the better behaviour.

- dropdown_controller.js (new) + manifest registration
- design.md: correct the stale Dropdown/popover note (there was no live dropdown
  controller) — document the native-<details> + controller pattern
- bulk_court_dates/new_spec (:js): open the More menu before clicking the action

Verified: casa_cases index / additional_index pass; the :js bulk-court-date flow
passes (its intermittent failure is the pre-existing time-travel flake, not this
change); erb-lint + standardjs clean.
…charts

Replace the jQuery + Chart.js canvas charts on /health with bespoke,
server-rendered SVG that meets the data-viz standards from this branch:

- Every series carries a distinct line style (solid / dashed / dotted / dash-dot)
  and marker shape on top of a validated categorical palette, so it reads in
  grayscale, print, and full CVD, not by color alone. The legend keys the line
  style + marker, not a color swatch.
- The bubble chart becomes an accessible heatmap table (day x hour, sequential
  single hue, the count in every cell).
- Each line chart has a table-view twin (real table, scope headers), the SVGs
  carry role="img" + title/desc, and every value is reachable without color.
- Stat tiles summarise the range with correct totals (sums for additive metrics,
  a distinct count for unique loggers, footnoted); sections fall back to an empty
  state when a metric has no data (a genuine zero shows 0, missing shows No data).
- Sentence case, responsive, WCAG AA contrast.

Mechanics:
- HealthController#index computes the series / heatmap server-side; the spec-tested
  JSON data endpoints are untouched.
- New HealthHelper renders the SVG / tiles / tables / empty states.
- New minimal "metrics" layout loads Tailwind (the page is unauthenticated, so it
  cannot use the casa_app shell that needs current_user).
- Retire app/javascript/src/display_app_metric.js and its two requires; nothing
  else used Chart.js.

Verified: health request spec passes in defined order (8 examples); the
random-order failure is the pre-existing CaseContact time-travel flake
(design-todo Phase 0), not this change. standardrb + erb-lint clean.

Follow-ups (design-todo): functional date-range filter, a Stimulus hover /
crosshair layer, a validated dark-mode palette, and dropping the now-unused
chart.js / chartjs-adapter-luxon from package.json.
Finish the metrics page:

- Date-range filter: a preset row (last 3 / 6 / 12 months) above the charts,
  driven server-side by a ?range= param (clamped to the allowed set, default 12),
  so it stays no-JS and rescopes every section (charts, tiles, footer totals,
  heatmap). The active preset is marked aria-current.
- Hover: a new chart-hover Stimulus controller draws a crosshair that snaps to the
  nearest month plus one tooltip listing every series at that point (value leads,
  name and colour key follow), with a highlight dot per line. Tooltip text is set
  with textContent and every value also lives in the table twin, so hover is
  enhancement, not the only path.

Mechanics:
- HealthController#index reads @range and passes it to the now-parameterised data
  methods.
- HealthHelper#metric_line_chart wraps the SVG with the controller and a JSON
  config value; metric_range_filter renders the preset links.
- A dedicated lightweight metrics.js entry loads Stimulus plus only the hover
  controller (no jQuery or Chart.js), added to the esbuild entryPoints; the
  metrics layout includes it.

Verified: health request spec 10 examples in defined order (adds filter presets +
range clamp); the Stimulus controller connects on the live page, injecting the
crosshair and per-series dots. standardrb, erb-lint, and standardjs clean.
These were only used by the retired display_app_metric.js (the old Chart.js
health charts, now bespoke server-rendered SVG). Nothing else imports them:
esbuild rebuilds cleanly and the JS suite passes (197 tests). luxon was present
only as the chartjs-adapter-luxon peer.
The account menu and the cases "More" menu are both native <details> enhanced by
the dropdown controller, but opening one did not close the other, so they could
be open at once and overlap. The outside-click handler only closed the other on a
mouse click; opening a second menu by keyboard left both open.

Close every other open dropdown when one opens, via the native toggle event, so
exactly one is open regardless of how it was opened (mouse or keyboard).

Verified with a real-browser system spec (open More, then the account menu, and
the More menu closes); cases index specs pass (7 examples); standardjs clean.
Make the cases index sortable by clicking a column header. Server-side via
?sort= / ?direction= (default case number ascending, columns whitelisted), so it
composes with the existing filters and Pagy and survives a reload.

- Case number, Status (active), and Transition aged youth (birth date) sort on
  plain columns; Next court date and Assigned to use correlated subqueries (the
  earliest upcoming court date; the first active volunteer's name), NULLS LAST.
- A reusable TableHelper#sortable_header renders each header as a link with
  aria-sort and a double-caret indicator whose active half is brand-coloured
  (up = ascending, down = descending), 4px from the label. Actions stay unsorted.
- Secondary sort by case number keeps pagination stable.

Verified: cases index / additional_index specs (10 examples, incl. clicking a
header to reverse, ignoring a bogus param, and rendering every sortable column
against Postgres); casa_cases request spec (74 examples); standardrb + erb-lint clean.
Add a search field to the filter bar: it matches the case number or an active
assigned volunteer's name (case-insensitive substring), server-side via ?search=,
composing with the selects, sort, and Pagy. Submits on Enter or blur (reusing the
filter bar's auto-submit); a clear (x) link removes the term.

- filter_casa_cases matches case_number ILIKE OR an EXISTS subquery over active
  case_assignments joined to users.display_name; the term is escaped with
  sanitize_sql_like and bound.
- The field leads the filter bar (leading magnifier, native search-clear hidden so
  there is one clear control); a "Showing N cases matching ..." line appears when a
  term is active.

Live as-you-type search is logged as a follow-up in design-todo.

Verified: cases index / additional_index specs (11 examples, incl. matching by case
number and by volunteer name); casa_cases request spec (74); standardrb + erb-lint clean.
Make the cases index work well on phones (verified at 400 / 768 / 1280 with a
headless browser; the page never overflows the viewport):

- Below md the table becomes stacked cards: case number as the title/link with the
  Edit action, then a two-column labelled grid (next court date, status, transition
  aged youth, assigned to). At md+ it stays the full sortable table.
- The filter bar is a two-column grid on mobile: the search spans both columns (so
  it always equals two filters plus the gap, measured 453 vs 454) and the four
  selects sit 2-up filling the row; it reverts to the inline row at lg.
- WCAG: the search placeholder moves slate-400 -> slate-500 (2.56:1 -> 4.76:1) and
  the leftover slate-400 table text (em-dash, "No") moves to slate-500. Every other
  text pair on the page already measured >= 4.5:1.

design-todo: record the column-visibility picker as intentionally not planned (only
six columns; a responsive layout is the better mobile investment) and add a per-page
responsive pass.

Verified: cases index / additional_index specs (11 examples); erb-lint clean.
Verified at 400 / 768 / 1280 (the page never overflows the viewport):

- Stat tiles are a 2-up grid on mobile (200px each), inline row at sm+.
- The heatmap keeps its contained horizontal scroll (a 24-hour by 7-day grid is
  inherently wide, and such 2D content is exempt from WCAG 1.4.10 reflow), but its
  day-label column is now sticky so the days stay visible while scrolling the hours.
- The SVG charts already scaled fluidly and the filter and legend already wrapped.

Verified: health request spec (10 examples, defined order); standardrb clean.
The KPI grids and lists already responded; the two roster / cases tables scrolled
on phones. Give them the same mobile-card treatment as the cases index: below md the
table becomes a stacked list inside the section card, at md+ the full table.

- Supervisor roster: each volunteer becomes a card (avatar + name / email + view,
  then the status pill, case count, and recent activity).
- Volunteer cases: each case becomes a card (case number + status pill, then last
  contact and the Log contact action).
- Admin dashboard already had no table (KPI cards + a list), so it needed no change.

Verified with a headless browser: both fit at 400 / 768 / 1280 with no page overflow;
status pills and text all measure >= 4.5:1 (WCAG AA at every breakpoint). Dashboard
specs pass (11 examples).
… auth)

Responsive sweep of the remaining migrated pages (notifications, edit
profile, and the casa_auth pages: sign-in, forgot/reset password, accept
invitation) confirmed every one fits at true phone, tablet, and desktop
widths (375/414/768/1024/1280). Measured with a CDP device-metrics
override, which bypasses headless Chrome's 500px minimum-window clamp, so
media queries evaluate at real phone width rather than a clamped 500px.
The health heatmap keeps its horizontal scroll plus sticky day-of-week
axis, the correct pattern for a density matrix (card-stacking would
destroy the visualization).

Contrast checks surfaced three AA failures on migrated text, now fixed:
- notification and patch-note timestamps: slate-400 to slate-500 (2.56:1 to 4.76:1)
- input placeholders on sign-in, forgot/reset password, accept
  invitation, and edit profile: slate-400 to slate-500
- auth aside footer fine print: white/60 to white/80 (4.63:1 over the gradient)
… docs

design.md: document the responsive dual-layout table pattern (full table
in `hidden md:block` plus a stacked-card list below `md`), the density-
matrix exception (the health heatmap keeps horizontal scroll with a
sticky axis rather than card-stacking), and a true-width verification
step in the migration playbook (measure with a CDP device-metrics
override, not `--window-size`, which headless Chrome clamps to ~500px).
design-todo.md: mark the responsive pass complete across every migrated
page, with the pages, widths, method, and the three contrast fixes.
Promotes the throwaway sweep script to a tracked, self-launching tool so
the migration playbook's verification step is actually followable. It
spawns headless Chrome, drives it over the DevTools Protocol with
Emulation.setDeviceMetricsOverride (which forces the real viewport width,
unlike `--window-size`, clamped to ~500px), and reports page fit plus the
widest element whose rendered right edge overruns the viewport at each
width. Point the design.md and design-todo.md references at it.

Usage: bin/measure-responsive.mjs [w,w,...] <file.html> [file.html ...]
CaseContact's occurred_at future cutoff is now a lambda, so it is
evaluated per validation instead of frozen at class-load time. The old
form captured `Time.zone.tomorrow + 1.day` once, so if the model first
loaded while a spec was time-travelled to the past, the cutoff froze and
unrelated later specs failed with "can't be in the future". Added a
travel_to regression guard that fails against the old class-load capture.

Added a SupervisorDashboard service spec (the supervisor landing built
this session had no unit coverage): #volunteers is active-assignments
only, the :no_cases / :on_track / :follow_up statuses, #needs_attention,
#stats (roster counts plus recent hours), and the Row label / hours /
avatar helpers.
Phase A of the Case show migration (reusable infra; no page flip yet).

- add-to-calendar: hydrates the add-to-calendar-button web component from
  data values on connect, the Stimulus replacement for the legacy jQuery
  div.cal-btn sweep (which stays for un-migrated pages). It uses a distinct
  data-controller hook so the two never double-hydrate, and hydrating on
  connect keeps it Turbo-safe. Jest-tested via a mounted Stimulus app.
- modal: a native <dialog> wrapper (open / close / backdrop-close) for the
  report-generation and thank-you modals, replacing the Bootstrap modal
  components on the Tailwind layout. Focus trap, Escape-to-close, and an
  inert background come from <dialog> itself, so there is no bespoke key or
  focus handling to maintain.
Phase A (continued). A show-specific Tailwind case-contact card and its
follow-up control, plus CaseContactDecorator#medium_icon (a bootstrap-icons
variant of medium_icon_classes, since casa_app loads bootstrap-icons rather
than the legacy LineIcons font).

The card is deliberately separate from the shared case_contacts/_case_contact
partial (still Bootstrap, used by the un-migrated contacts index / drafts /
form); it will be consolidated when those pages migrate. It keeps the
.followup-button and followup-button-<id> hooks so the existing jQuery reminder
prompt still binds, and exposes a data-case-contact hook for the show spec's
count. It is wired into the page in Phase B.
Phase C infra: the Stimulus pieces the migrated report-generation flow needs,
so the upcoming page flip has no jQuery or Bootstrap-modal dependency.

- court-report: posts the date range to the JSON endpoint (Rails wraps the flat
  body under case_court_report), shows a spinner, and opens the docx in a new
  tab. Replaces the legacy handleGenerateReport, which toggled Bootstrap d-none
  and listened for .bs.modal events.
- local-storage-reset: clears a localStorage key on connect, discarding the
  saved case-contact draft on the success redirect. Jest-tested.
- modal: an openOnConnect value, so the thank-you dialog can auto-open on the
  success redirect.
Flip casa_cases#show onto casa_app with a Tailwind rebuild: header actions, a
"Case details" fact card (transition, date in care, next court date with
add-to-calendar, court report status + download, court orders, placement), a
court dates card, assigned volunteers with the reminder form, and the
case-contact list using the new Tailwind card.

The report-generation modal is now a native <dialog> driven by the modal +
court-report Stimulus controllers, and the thank-you dialog auto-opens on the
success redirect (clearing the saved draft), so the page no longer needs
Bootstrap modal CSS or the inline <script>. Add-to-Calendar buttons use the
add-to-calendar controller.

Show-specific partials (contact card, reminder form) avoid mutating the shared
Bootstrap partials still used by un-migrated pages. Existing label text is kept
so content specs stay green; specs coupled to the old markup are updated: the
report-modal hooks, the .card-content count becomes data-case-contact, and the
"CASA Case Details" / "Case number:" markers become the new h1 and section title.
White text on emerald-600 was 3.77:1 (fails AA for text); emerald-700 is
5.48:1. Bumped the resolve-reminder button background to emerald-700
(hover emerald-800). Measured with the WCAG contrast formula.
design-todo.md: mark Case show migrated (approach, infra added, and the
follow-up to consolidate the show-specific contact card / reminder form when
the contacts index and volunteers-edit migrate); note the now-orphaned
_generate_report_modal in the dead-code cleanup item.
design.md: add the Modal (native dialog) component pattern.
Case groups (index, form, controller) move to the casa_app Tailwind layout,
with a TomSelect theme so the multiselect matches the design system on casa_app.

Establish two shared tokens so styling stops drifting:

- button_classes(:primary | :secondary | :danger) in DesignSystemHelper is the
  single source of truth for buttons, replacing five copy-pasted class locals.
  Every variant shares a fixed h-10 (40px) height, so box-sizing: border-box
  absorbs the outlined variant's 1px border and all variants are the same height
  by construction, with no fragile border border-transparent compensation.

- One native-<dialog> modal template: header (optional 32px status badge, title,
  close), divider, body, divider, right-aligned footer. The delete confirm
  (shared/_confirm_button), the court-report modal, and the success dialog now
  share it. The status icon is one rounded-full badge in two sizes (32px inline,
  48px hero). tailwind.css re-centers the dialog because Tailwind's reset drops
  the browser's centering margin.

WCAG: the modal close icon moves from slate-400 (2.56:1) to slate-500 (4.76:1).

Specs: a DesignSystemHelper#button_classes unit spec and a case-groups
delete-via-confirmation-dialog system test. design.md documents both tokens.
The court-report, success, and delete-confirm modals were three copies of the
same inline <dialog> markup: the same drift the button token just fixed. Fold
the template into a Dialog:: ViewComponent suite (the native-dialog counterpart
to the Bootstrap Modal:: suite):

- Dialog::GroupComponent: the <dialog> shell (trigger slot, size, aria label,
  modal controller wiring, extra controllers, open-on-connect, extra data).
- Dialog::HeaderComponent: optional status badge, title, and close button.
- Dialog::BodyComponent: panel or centered status body, with extra classes.
- Dialog::FooterComponent: a divided action row, end- or center-aligned.

Header/body/footer render as standalone components so they compose even when a
form_with wraps them (the court-report modal). shared/_confirm_button and the
two case-show modals now use the suite; markup and geometry are unchanged
(verified centered, 40px buttons, 32px badges). Specs cover all four components,
and design.md documents the suite.
The case-show header was a flat row of up to five buttons. Adopt the cases
index's header pattern: one primary CTA plus a More overflow menu.

- New Case Contact (primary), Edit Case Details, and Emancipation (when the
  youth is transition-aged) stay visible; Generate Court Report and New Fund
  Request move into a More disclosure (native <details> + the dropdown
  controller), matching the index.
- Core actions stay visible on purpose: burying Edit or Emancipation would cost
  a click on common actions and break non-JS specs, since rack_test cannot open
  a native <details>.
- Dialog::GroupComponent gains a wrapper_class option; "contents" lets the
  court-report modal's trigger and dialog drop straight into the menu as an
  item. The report modal is extracted to casa_cases/_court_report_modal.
- Menu-item leading icons top-align to the first line (items-start).
- The index's More summary now uses button_classes(:secondary), matching the
  40px primary and fixing a height mismatch introduced by the button token.

design.md and CLAUDE.md document the header pattern and the icon alignment.
The header keeps New Case Contact (primary) and Edit Case Details visible and
now overflows Generate Court Report, Emancipation, and New Fund Request into the
More menu.

The emancipation-link check becomes :js and opens More first, since the link now
lives in the native <details> menu. design.md updated to match.
On phones the header wrapped into a messy multi-row of buttons. Now only the
primary CTA and More share the top line; Edit collapses into More below sm.

Edit renders twice with responsive visibility: a button wrapped in
hidden sm:contents (sm+) and a sm:hidden menu item (mobile). It stays no-JS,
is unambiguous (one visible Edit at any width), and the non-JS Edit test still
finds the visible button. design.md and CLAUDE.md document the pattern.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Touches dependency files erb Touches ERB templates javascript Touches JavaScript code ruby Touches Ruby code 🧪 Tests Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant