Part2 of design rebuild - DO NOT MERGE YET - #7087
Conversation
…7051 Starting point for the part-2 tracking PR. Five items, each verified still open against main rather than copied forward from notes: - the 3 xit'd flaky case-contact system specs, which need a tracking issue before they can be re-enabled - two view specs still raising SystemStackError from the settings rail calling edit_casa_org_path(current_organization) unstubbed (pre-existing, still red) - sort still living on the filterrific form, which is why Clear filters has to carry sorted_by by hand - mailers naming the chapter inconsistently (@casa_organization vs @casa_org vs nothing on Devise) - contact types on the case-contact form staying an exposed checkbox fieldset, with the reason it is deliberate Dropped one item while checking: the read-notification icon is already slate-500, so its contrast was fixed in the merged work and is not outstanding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Editing a case contact could silently lose work. Autosave was bound per field, to notes, topic answers and expense descriptions only, so an edit to duration or medium was dropped on navigation -- UNLESS the user also happened to touch one of those three, because an autosave posts the entire form and therefore committed everything. Whether your work persisted depended on which field you touched last, with no feedback either way. Measured before: edit the duration, wait past the debounce, click Back -> old value; edit the duration then type one character in notes -> both saved. Bound once on the <form> instead: `input` bubbles and fires for every control type, so text, number, date, select, checkbox and radio all save, and a field added later cannot be forgotten. The three per-field triggers are now redundant and removed. Verified each control type saves on its own and survives leaving: duration 60 -> 210 with nothing else touched and still 210 after Back; a radio choice; a contact- type checkbox. Also verified checking a topic -- which now triggers an autosave too -- still yields exactly one ContactTopicAnswer, i.e. it does not race the create that stores the answer's id. This is why the form has no Cancel and no unsaved-changes warning: with everything autosaved there is nothing unsaved to warn about. The two coherent models are full autosave (Google Docs / Notion: navigation is the exit) and explicit save (GitHub / Jira: Cancel plus a dirty guard). A partly-autosaving form was neither. One measurement trap, documented: the "Saved!" alert lingers ~3s, so a second interaction in one example matches the previous save's alert and reads the DB before its own debounce elapses -- which made checkboxes look broken when they were not. The new specs do one interaction each and wait on the alert. case_contacts 148 examples 0 failures; the 3 new examples pass 3/3 repeat runs. standardrb + erb_lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e org
`MileageRatesController` authorized the CasaAdmin *class* for new/create/edit/
update. Two problems:
- `GET /mileage_rates/:id/edit` raised NoMethodError: undefined method
'casa_org' for class CasaAdmin. Pundit resolved the class to
CasaAdminPolicy, whose #edit? is the one method there that reaches
record.casa_org -- fine for a CasaAdmin instance, fatal for the class. The
page 500'd for every admin in every org. #update survived only because
CasaAdminPolicy aliases update? to index? (is_admin?, never touches the
record).
- Authorizing a class skipped the org check, and `set_mileage_rate` found by
id unscoped, so an admin could edit another org's rate.
Authorize the record against a new MileageRatePolicy (is_admin_same_org?) and
scope the finder to current_organization. Behaviour is otherwise unchanged.
The edit action had no request spec -- only a system spec asserting the "Edit"
link's text, never following it -- which is why this shipped. Adds GET /edit
coverage plus cross-org rejection for edit and update, and puts the update
block's rate in the admin's org (those examples were passing *because* of the
missing org check).
Found while running a whole-app WCAG audit: the page's axe run reported
document-title/html-has-lang/landmark-one-main/region, which is the signature
of a layout-less Rails error page rather than an accessibility defect. Behind
the crash the page audits clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An axe-core (WCAG 2.1 A/AA) sweep of all 80 pages across the four roles found violations on 12 admin pages; the volunteer, all-casa and signed-out surfaces were already clean. Fixes, by rule: heading-order (7 pages) -- an <h6> was used as the subtitle directly under the page <h1>, skipping h2-h5, in court_dates/_form, placements/_form and bulk_court_dates/new; those are captions, so they are now <p>. In court_dates/show the fact labels were <dt><h6>Judge:</h6></dt>: a <dt> is already the term, so the nested heading is gone and "Court orders:" -- a real section heading -- is an <h2>. case_contacts/_case_contact now takes a heading_level local (default 3): #index nests cards under an <h2> case-number section, #drafts has no grouping level and passes 2. label (critical) -- the three import file inputs had no accessible name. Their ids are overridden for the import JS, so the <label for> points at the rendered id, per the pattern already documented in design.md. aria-input-field-name + scrollable-region-focusable -- <trix-editor role="textbox"> on the banner form. label/for only associates with form-associated elements, so the correct-looking visible <label> never named the custom element; it now carries aria-label. Trix also ships its toolbar row as `overflow-x: auto` with nothing focusable, so tailwind.css lets the row wrap instead of scroll (also fixes buttons hiding behind a scrollbar-less scroll on narrow screens). link-in-text-block -- the case-number link inside the placements <h1> was brand-600 on slate-900, 2.83:1 against the surrounding text where 3:1 is required, with no other cue. Underlined. color-contrast -- emerald-600 is 3.67:1 on white: fine for a decorative icon (3:1) but short of 4.5:1 for the 12px "+N vs last month" delta on analytics. Measured every neighbouring token from the built oklch values and recorded the table in design.md; amber-600 (3.19:1) is worse and slate-400 is 2.63:1. Moved the analytics delta pair to emerald-700/rose-700 and the org-settings contact topic chips to emerald-700/slate-500. Two of these were invisible to the first pass because axe only sees the rendered DOM and the fixtures seeded no rows: the emancipation checklist's category and option inputs had no accessible name at all, and the org-settings status chips never rendered. The checklist inputs get aria-label rather than a real label -- the JS sets checked state after an AJAX save, so an associated label would toggle natively on top of it. Extends spec/system/accessibility/axe_spec.rb to cover all 14 fixed pages plus the emancipation checklist, each seeding the rows its page needs, so these cannot regress. Ran the sweep again afterwards: 80/80 clean. Not fixed here, and pre-existing on a clean tree: the two SystemStackError view specs (mileage_rates/index, casa_admins/admins_table) and Selenium races in bulk_court_dates/new_spec and casa_cases/edit_spec, all of which fail intermittently with or without this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…points
The first sweep ran at one desktop width, which cannot see this app's mobile
markup at all: axe skips hidden elements, and every list page renders a
`md:hidden` card list beside its `hidden md:block` table. Re-running the same 81
pages at 390px surfaced six violations that 1400px reported clean.
page-has-heading-one (3 auth pages) -- the only <h1> lived in
layouts/casa_auth's `<aside class="hidden … lg:flex">`, so below lg the sign-in,
password-reset and all-casa sign-in pages had no h1 whatsoever. The page's
subject is the form, not the brand line, so each form heading is now the <h1>
("Welcome back", "Reset your password", "All CASA log in", …) and the marketing
statement is a <p>. Applied across sessions, passwords, invitations and
confirmations so the auth surface is consistent.
color-contrast -- the lg:hidden org-settings group labels were slate-400
(2.63:1) at 12px.
scrollable-region-focusable -- metric_data_table and metric_heatmap wrap their
tables in `overflow-x-auto`, which becomes a real scroll container once the
table stops fitting. Both hold pure data with no links or controls, so nothing
inside could take focus and the region was unreachable by keyboard; the scroll
containers now carry tabindex="0".
Separately, /case_contacts/new_design is gated behind the
:new_case_contact_table Flipper flag, so walking the routes just followed the
redirect and never audited it. With the flag on it renders "Reached" /
"Not reached" in emerald-600 (3.65:1) and amber-600 (3.19:1) as 14px text --
both moved to -700, matching the fix already applied elsewhere.
Sweeps now clean at 390 / 768 / 1400 for all 81 pages. axe_spec.rb gains a
mobile-viewport context (it resizes, then restores) covering the auth pages,
org settings, analytics, the two index pages with mobile card lists, and the
flag-gated table, so none of this can regress from a desktop-only run.
design.md records the viewport rule, the auth heading structure, the
tabindex-on-scroll-container pattern and the flag-gated blind spot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… flaky
The example clicked "Create" and immediately visited another page. `click_on`
does not wait for the POST or its redirect, so the navigation could still be in
flight when `visit casa_case_path` fired, and whichever won decided which page
the assertions ran against. When the redirect won, the assertions ran against
the bulk form we had just left.
Its first assertion could not catch that, because `hearing_type.name` also
appears in that form's own "Hearing type" <select>: have_content matched an
<option> on the stale page, returned immediately, and only the court-order
assertion failed -- which is why the failure looked unrelated to navigation.
Measured in isolation on a clean tree, this failed 5 runs out of 10, every one
of them with the same signature: have_content(court_order_text) against a page
whose text still read "1 court date created!". So:
- wait for the create to land (the flash) before navigating away;
- anchor on the case page's <h1> before asserting its contents, so a stale
page cannot satisfy the assertions;
- scope both assertions to the elements that carry the values -- the
court-date row link and the court-orders table -- so no <option> or
unrelated copy can satisfy them.
Also stop caching a raw Selenium reference for the court-order textarea. The row
is cloned from a <template> by the court-order-form controller, so a `.native`
handle taken from `first` while that insertion settles can go stale; Capybara's
find/#set re-resolves the node and waits. The textarea already carries
aria-label="Court order text", so it has a stable locator (Capybara's aria-label
lookup is off in this suite, hence the attribute selector).
Verification, no retries: the reproducer batch
(bulk_court_dates + court_dates + placements + banners --seed 12650) failed on
every baseline run and is now clean across ~26 runs; the wider 11-directory
batch went from 1 of 3 clean to 3 of 3. In isolation, 5/10 -> 2/20, and the two
residual failures are a different, pre-existing problem: Chrome raises
"Node with given id does not belong to the document" when Capybara reads the
document mid-Turbo-swap. That is a driver-level error rather than a spec bug --
Selenium raises a non-retryable UnknownError instead of a stale-element error --
and it also hits court_dates/new_spec and placements/edit_spec. It is covered by
this repo's CI policy (spec/support/rspec_retry.rb retries :js examples 3x on
CI): under CI=true this file passed 20/20 without needing a single retry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ent CDP errors
Follow-on from the bulk_court_dates fix. Every one of these was an assertion or
element read that could not wait, so a slow page swap failed the example rather
than being retried. Measured rates below are without retries (rspec-retry is
CI-only here).
court_dates/new_spec -- same cached `.native` handle as bulk_court_dates: the
court-order row is cloned from a <template> by the court-order-form controller,
so a raw Selenium reference taken from `first` while that insertion settles can
detach before send_keys runs. Uses Capybara's finder and #set instead.
placements/edit_spec -- the same defect the bulk spec had. `click_link("Edit")`
was followed by assertions on the case number, but the placement *show* page we
came from also renders the case number, so have_content("123") could be
satisfied before the navigation finished and the form interactions then raced the
page swap. The before block now anchors on the edit page's <h1>.
casa_cases/edit_spec (2 examples) -- `expect(assign_badge.text).to eq
"Unassigned"` read .text off an already-found element and compared it with a
plain Ruby ==, which never retries, while the badge updates asynchronously after
"Unassign volunteer". Now a waiting matcher with exact_text, preserving the
whole-string comparison. 2/12 runs -> 0/16.
other_duties/new_spec -- `find("#other_duty_notes").native.attribute(
"validationMessage")` sits outside Capybara's synchronize, so the
StaleElementReferenceError raised while the page settled went straight through,
even though stale-element is in Capybara's retry list. Capybara has a
:validation_message node filter for exactly this; as a matcher filter it
re-resolves the field on every attempt. Routing through Element#[] would not
work: Capybara::Selenium::Node#[] rescues WebDriverError and returns nil, so the
staleness would become a silent nil. 3/20 -> 0/20.
Adds spec/support/capybara_transient_node_retry.rb for the one failure mode that
is not a spec bug: Chrome raises
UnknownError: unhandled inspector error:
{"code":-32000,"message":"Node with given id does not belong to the document"}
when Capybara queries the page mid-Turbo-swap. It is transient and semantically
a stale element, but Selenium reports a generic UnknownError and Capybara's
retry loop matches by exception class, so it was re-raised immediately. The patch
matches that single message inside Capybara's existing synchronize loop, so it
reloads and retries until default_max_wait_time and a genuinely stuck page still
fails, just at the wait timeout. Adding UnknownError to invalid_element_errors
would have been far too broad; unit-checked that other UnknownErrors are still
re-raised.
Verification: the previously flaky files at 0/20 each; the four-directory
reproducer clean across 12 random-seed and 5 fixed-seed runs; and the full
spec/system suite (713 examples) clean 4 runs in a row, where it had been
averaging about one failure per run on a different spec each time. Untouched:
the .native read in all_casa_admins/patch_notes/index_spec, which is a static
textarea already scoped by `within` and has not been observed failing, and the
two pre-existing SystemStackError view specs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Banner (layouts/_casa_banner). The megaphone icon was text-amber-500, which is
2.07:1 against the amber-50 banner background where WCAG 1.4.11 (non-text
contrast) needs 3:1. It now inherits the container's amber-900 (8.77:1), matching
shared/_flashes where the severity icon takes the container colour rather than
setting its own. Measured in a real browser, not read off the classes.
This is the class of defect the whole-app axe sweep could not find: axe has no
rule for non-text contrast, so the dashboard audited clean with a 2.07:1 icon on
it. design.md now says icon contrast has to be checked by hand, and against the
tinted surface rather than white.
The Dismiss control was `<a href="#">`. It performs an action rather than
navigating, so a link mis-announces it to screen readers and does not activate on
Space; it is now `<button type="button">`, underlined for a non-colour-only
affordance, with a focus-visible ring. amber-700 on amber-50 is 4.87:1. Capybara's
click_on matches buttons and links alike, so banners/dismiss_spec is unaffected.
The banner content was already fine at 8.77:1.
Flash messages (shared/_flashes). Success messages now clear themselves after ~6s
via a new auto-dismiss controller; warnings and errors stay put, since they are
often the only record of what went wrong. The timer pauses on hover and focusin so
a message cannot vanish mid-read, which with the delay keeps this clear of WCAG
2.2.1; role="status" is unchanged, so it is still announced on arrival. The fade
is an inline style and the removal is on a timer rather than transitionend: a
class added from JS only works while Tailwind still emits it, and a missed
transitionend would leave the message up forever.
Two things found while doing it:
- "Signed in successfully." only appears when a return path was stored. Devise
prepends allow_params_authentication!, so when Accessible#check_user calls
current_user during POST /users/sign_in it authenticates from the params;
current_user is therefore truthy in a before_action on create, check_user runs
flash.clear and redirects, and Devise's create -- which sets the message --
never runs. With session[:user_return_to] set it is skipped and the message
survives, which is the flow where it is seen. Left as-is: making it appear on
every sign-in is a product call, not part of this fix.
- Authorization failures were being sent as flash[:notice]. The partial maps
notice to the green success variant, so a permission error already rendered as
a success, and auto-dismissing success would have quietly thrown it away.
They now use flash[:alert] (amber, role="alert", no auto-dismiss) in all three
places that produce that message: ApplicationController#not_authorized and the
cross-org RecordNotFound rescues in CasaCasesController and
CourtDatesController. Request specs asserting the key move with it; the system
specs already asserted `.alert`, which every flash div carries.
Covered by spec/system/layouts/flashes_spec.rb (auto-dismiss, hover pause, and an
authorization error staying on screen). Verified: full spec/system 716 examples
clean twice, spec/requests + spec/controllers 1000 examples clean three times,
jest 126/126, standardrb/erb_lint/standardjs clean. Adds the patch_notes `.native`
cleanup to design-todo.md Part 2.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The roster's "Volunteers" cell held three count pills -- N attempting /
N not attempting / N transition-aged -- with the first two omitted at zero. A
pill carries a categorical status, not a quantity, and using them for counts cost
all three properties a numeric column gives you:
- Ragged metrics. Because the leading pills were conditional, the same metric
sat at a different x-offset on every row: transition-aged measured at
787 / 651 / 673px across three rows, a 136px swing, so the column could not
be scanned. As columns all four right-align exactly (648 / 783 / 948 / 1115
on every row).
- Unreadable at a glance. Three multi-word chips with icons at 12px in one
cell; the figures are now 14px plain numerals with the words in the header.
- Dead ends. The counts linked nowhere.
Now: Volunteers | Attempting | Not attempting | Transition-aged, right-aligned
with tabular-nums so digits line up, zero rendered as a muted 0 rather than
omitted (a missing number is not the same as zero, and dropping it is what made
the rows shift). Volunteers and Transition-aged link to the filtered volunteer
list, which volunteers#index already supports via its supervisor and transition
params. Attempting/not-attempting stay plain text: there is no contact-activity
filter on that page to link to, and adding one is a bigger change than this.
Not attempting keeps weight and rose colour when non-zero -- it is the number
worth acting on -- as reinforcement only, since the column header names it.
Converting pills to columns has a cost worth recording: wrapping pills fit a
narrow viewport, fixed columns do not. Measured, the table went from 341px (no
scroll) to 616px in a 341px viewport, so the change introduced horizontal
scrolling on mobile. A data table is the WCAG 1.4.10 Reflow exception and axe
stayed clean at 390px either way, so an audit would not have caught it. Fixed
with this page's existing pattern: the table is hidden md:block and a md:hidden
card list repeats the figures in a labelled dl grid. Verified no two-dimensional
scrolling and axe clean at 390 / 768 / 1400. The counts are hoisted into one
array so the two copies do not each call no_attempt_for_two_weeks, which walks
every volunteer's contacts.
The table gains a sr-only <caption> and scope="col" headers, which it lacked.
Specs: the view spec asserted the omit-at-zero behaviour ([data-stat=...].length
== 0), which is the behaviour that caused the misalignment; those now assert the
value is 0, which is what they were really about. The no-volunteers hook moves
onto the zero total cell, so that assertion is unchanged. Full spec/system 716
examples clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… way back
Two problems with the numeric-column change, both of them design-system rules
that are already written down.
1. The non-zero "Not attempting" count was text-rose-700. rose *is* the semantic
"needs follow-up" token, but the system spends it on status pills and danger
buttons -- not on bare coloured digits in a data cell, where it makes colour
carry the meaning and reads as an error state rather than a figure. Emphasis is
now weight only (font-semibold text-slate-900, muted slate-500 at zero); the
column header already says what the number is. A pill would be the legitimate
status treatment but belongs in its own column -- in the numeric column it
would break the digit alignment the column exists for.
2. The counts linked into the volunteers list with no route back, which design.md
already calls a flow trap under Names. Fixed along the whole chain, since a
half-fix strands the user one hop later:
- the drill-through links carry from=supervisors, this app's existing
convention for origin (volunteers/edit already reads from=other_duties and
from_case_id);
- volunteers#index renders the documented chevron only when params[:from]
says so -- it is a top-level nav page, so a permanent back link would be
wrong. It uses the "title + actions" header shape (back link + h1 mt-2 as
the left column) rather than shared/_page_header, which is deliberately the
simple back+title shape;
- the filter bar carries the origin in a hidden field, or the back link
disappears the moment the user filters (shared/_pagination was already safe,
it merges request.query_parameters);
- the per-volunteer links carry it one hop further, so the edit page returns
to the filtered list rather than the unfiltered roster.
Walked end to end in a browser: roster -> filtered list -> volunteer -> back to
the filtered list -> back to the roster, and absent when arriving from the nav.
Also consolidated the links onto record_link_class. The count links were a
hand-rolled string with a persistent underline; that treatment is reserved for a
record link inline in body text, and hand-rolling it is how the placements
case-number link I added earlier shipped without a focus ring. Both fixed, plus
the two other hand-rolled brand links in this file.
Spacing/alignment audited by measurement rather than eye, against the sibling
table and mobile card on the same page: card padding 8px 0, th/td 12px 16px,
mobile card 16px, mobile dl 12px/16px with mt 12px -- identical on both. Numeric
th and td both right-aligned with font-variant-numeric: tabular-nums, and the new
back link sits 9px above the h1, the value design.md records as pixel-verified.
The new spec initially failed two ways worth noting: it needed :js (the filter
bar auto-submits from Stimulus, and under rack_test both the desktop table and the
md:hidden card list count as visible, so the volunteer name matched twice), and
its barrier was have_link("Back to supervisors"), which is true on the page it
came from too -- the same non-synchronising assertion fixed in
bulk_court_dates/new_spec. It now waits on the new URL and scopes the click.
Full spec/system 719 examples clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four count columns had ended up with four different treatments in four
adjacent cells: brand-600 + font-medium where the figure happened to be a link,
plain slate-900, semibold slate-900 on the "interesting" one, and muted
slate-500 for any zero. Three unrelated signals -- colour meaning "is a link",
weight meaning "is worth acting on", muting meaning "is zero" -- were competing in
the one place whose entire purpose is comparing figures across and down. A reader
cannot tell whether blue-vs-grey encodes magnitude, status or navigability, which
is exactly what made the table hard to scan.
Every figure now renders identically: the table body colour (text-slate-700),
weight 400, 14px, right-aligned, tabular-nums, zeros included, no per-cell
variation. Verified rather than eyeballed: across every cell at both viewports
there is exactly ONE computed style,
oklch(0.372 0.044 257.287)|400|14px|right|tabular-nums, and no numeral is a link.
The drill-through moves off the numerals into ONE row-level action ("Volunteers",
ghost_class, next to Edit -- the two-ghost-action shape the cases table on this
page already uses). Linking the figures also advertised an asymmetry in the data
model in the worst possible place: only two of the four counts could link at all,
because volunteers#index filters by supervisor and transition age but not by
contact activity, so Attempting and Not attempting had nowhere to point. One
action per row removes that entirely. The return path is unchanged -- the action
still carries from=supervisors, so the destination still offers "Back to
supervisors".
The transition-aged drill-through collapses into that single action; the
destination page has a Transition-aged filter, so the narrower view is still one
control away.
design.md now records the rule and, deliberately, the two rejected versions --
each looked defensible on its own and the problem only appeared when the row was
read as a whole.
Full spec/system 719 examples clean; the drill-through spec now asserts no
numeral is a link and clicks the row action instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…stacked boxes
All three dashboards rendered every attention row as its own rose-tinted,
rose-bordered rounded-xl panel with a filled 40px rose icon tile -- nested inside
the section card. Two things go wrong, and both get worse the longer the list:
- Card-in-card. The section card is already the container; repeating it per row
is the nested-card anti-pattern Material calls out for continuous lists.
- A tint on every row signals nothing. Alert fill is for a *single* message.
Applied to a repeating collection it stops meaning "urgent", becomes the
background, and fights the card it sits in. Severity belongs at the section
level, stated once by the heading and its count.
Rows are now a divided list in one container (ul.divide-y divide-slate-100, rows
py-3), matching the in-card list already used on casa_cases#show: primary text,
an optional text-xs context line, and one action on the right. Measured at six
rows, before -> after: 6 self-boxed rows -> 0, 12 rose-tinted elements -> 0,
6 icon tiles -> 0, a stray rule under the last row -> none, 528px -> 483px.
The industry answer to "table or list": a list when each row is an entity plus a
little context plus an action (Polaris ResourceList); a table only when rows carry
several comparable attributes worth scanning column-wise. These are the former, so
they stay lists.
Also removed a real defect on the supervisor dashboard: two adjacent ghost buttons
with the same href ("Send reminder" and "View", both the volunteer page). The verb
survives -- deep-linking it to where the action happens is what these dashboards
already do ("Assign a volunteer" -> the case page) -- and the volunteer's name is
identifying text rather than a second link to the same place, per the names rule.
Admin/volunteer rows keep their case-number record link plus one action, which is
a different shape on purpose: a record is a record link, a person's name is not.
Unrelated flake fixed while verifying: volunteers/edit_spec asserted a note
creator's raw display_name against a view that strips honorifics, so it failed
only when Faker generated a prefixed name ("Ms. Salvatore Cummings"). It now
compares NamePresentation.strip_honorific -- 1/5 runs failing -> 0/8. (An
intermediate attempt called the formatted_name *helper*, which isn't in scope in a
system spec: 8/8.)
Guarded by a new spec asserting the list is one divided list with no per-row fill,
border, rounding or icon tile. Full spec/system 720 examples clean twice; axe clean.
design.md documents the pattern, both failure modes, and the list-vs-table call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…0% void) Last change stripped the per-row tinted boxes, which was right, but left the rows as a divided list stretched across a full-width card. Measured, that is why it then read as a big empty table: admin row 918x53, ONE line of content (a case number), 645px void supervisor row 918x67, two lines, 547px void justify-between pins two small items to opposite edges, so ~70% of every row was air. The tinted boxes had been hiding that by filling it with colour. A divided list is the right treatment in a narrow column and the wrong one in a wide card. So each worklist is now the same desktop-table + md:hidden-divided-list pair the sibling tables on these pages already use, with columns that spend the width on data. Largest gap between columns after the change: 0px. admin Case | Next court date | action supervisor Volunteer (avatar + name) | Cases | Last contact | action volunteer Case | Last contact | action Two data columns is the minimum, or the void just reappears in table clothing: the admin worklist only had a case number, so it gains Next court date, which is also the thing you triage on -- an unassigned case with court imminent is the one to staff first. AdminDashboard documents "no per-case queries", so that is one grouped minimum(:date) lookup (next_court_dates), not CasaCase#next_court_date per row: 3 queries for the whole section, verified by counting. Empty states are unchanged and already followed the documented patterns -- the all-caught-up emerald panel (pattern 2, "Every case has a volunteer" / "You're all caught up", 960x145) and the cold-start welcome panel (pattern 1, 512x352). They now carry the table card's m-4 inset. Verified at 390px and 1400px: correct variant visible, no two-dimensional scrolling, axe clean at both. spec/system + spec/services 846 examples clean. The regression spec now asserts real columns rather than a stretched list. tmp/seed_dashboard_attention.rb gives the demo cases upcoming court dates (one left blank to show the "None set" state). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two bits of drift in the dashboard worklists I built last change, both because I
matched a neighbouring table's markup instead of checking it against design.md's
table pattern -- which propagates drift rather than catching it.
1. Double divider. A card with a title block already carries
`border-b border-slate-100 p-4`, so the table's `thead` <tr> must not add a
second: two hairlines ~40px apart read as a mistake. Absent a title block (the
cases / supervisors / volunteers index tables) the thead's border IS the
separator and stays -- verified both cases by measuring the rules above the
column headers: 1 in a titled card, still 1px on the index tables.
Audited app-wide rather than only fixing mine: 6 sites, three of them
pre-existing -- the supervisor "Your volunteers" and volunteer "Your cases"
tables and volunteers/_notes had been doubling it too.
2. Hand-rolled brand links. "All cases" was a bespoke string ending in
`focus-visible:underline`, i.e. no focus ring, where design.md says the two
in-content link treatments are name_link_class (people) and record_link_class
(records) with callers prepending size/weight. It was pre-existing, but I
rebuilt that header and should have caught it -- especially having consolidated
this same drift onto record_link_class earlier. Also fixed in the same family:
"See all placements", and the two "Log contact" row actions in the volunteer
dashboard's own table, which were bespoke while every other row action on these
dashboards (including the ones I had just added beside them) is ghost_class.
Left alone deliberately, and listed in the commit for a decision rather than swept
silently: 18 further hand-rolled brand links outside this family -- 7 on auth pages
("Back to sign in", "Forgot password?"), 3 footer credits, 3 imports CSV downloads,
and 5 one-offs. They are inline body links in other contexts; several also lack a
focus ring, so they are worth a separate pass.
tmp/dashboard_state.rb replaces the earlier seed script and flips the admin
dashboard between all three states so each can be seen in a browser --
`populated` / `empty` / `cold` / `status`. Only touches DEMO-* rows and a separate
"Demo Empty Chapter", so the real seed data is untouched. All three verified by
fetching the running app: worklist table with 5 rows, the "Every case has a
volunteer" panel, and the "Welcome to your chapter" cold start. The cold-start
admin needs confirmed_at set -- User has Devise :confirmable, so an unconfirmed
account silently lands on the public marketing page instead of the dashboard.
Fixed a flake in my own spec from last change: it asserted no `bg-rose` anywhere in
the section, but an initials avatar is legitimately `bg-rose-100` (avatar_color
cycles a palette by volunteer id), so it failed 2 runs in 8. Now scoped to the row
and its cells -- 0/10.
spec/system 720 examples clean; only the two known pre-existing SystemStackError
view specs still fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported as "banners do not work, when I create one it does not load". Creation
and display both work: driving the real form with Active? ticked produces
active=true, deactivates the previous banner, and renders on the dashboard. The
problem is that `active` defaults to false, so a banner saved without ticking that
checkbox is invisible by design -- and nothing anywhere said so. The dev database
had exactly that: three banners, the two most recent both active=false.
Three things hid it, all fixed:
- `create`/`update` set no flash at all. You were redirected to the list with no
indication of the outcome. They now report it -- a :notice (green,
auto-dismisses) when the banner is live, an :alert (amber, stays put) when it
is not: "Banner created, but it is not active, so no one will see it yet. Use
Activate to show it." The amber/persistent choice is deliberate; that is the
message that has to be read.
- The list reported state as plain body text, "Yes"/"No", in the same weight and
colour as every other cell. It is now the documented status pill (emerald
check / slate minus).
- Activating meant opening Edit and finding the checkbox. Each inactive row now
has an Activate action, which is safe as a one-click: only one banner can be
active and the controller already deactivates the alternate.
Also removed `thead class="bg-slate-50"` from this table -- design.md calls that
out by name ("never a bg-slate-50 fill", a reimbursements-table drift) and it had
crept back. It survives in 6 other tables (placements, mileage_rates,
court_dates/show, emancipation_checklists, all_casa_admins casa_orgs/show and
dashboard/show); not swept here since this turn is about banners.
Three spec assertions moved off the old markup: two asserted a whole row as one
flat string ("Expiring Announcement Yes in 7 days"), which the pill breaks into
lines, so they are now scoped to the row and assert the cells.
Fixed a race in my own flashes_spec while verifying: sign_in_via_form clicked
"Sign in" and returned with no barrier, so the following `visit` could beat the
POST's redirect and the authorization flash under test belonged to the wrong page
(1 run in 6). It now waits for Devise's stored return path -- 0/10. That is the
same non-synchronising pattern fixed in bulk_court_dates and volunteers/index this
session; I wrote it again two turns ago.
New spec covers the whole path: create without Active -> amber alert + Inactive
pill + Activate action -> click Activate -> Active pill, active in the database,
banner renders. spec/system 721 examples clean twice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Following the banners fix, which noted the forbidden thead fill survived in 6
other tables. It was worse than that -- the whole token set had drifted, in two
directions at once, so a fill-only fix would have left tables with no separator or
with two.
Swept app-wide and audited statically over app/views/**/*.erb rather than by
sampling pages:
- 15 tables carried `divide-y divide-slate-200` on the <table> element. That is a
darker rule than the token, and where the header row also had a border it was a
second rule. Removed; the header row's border-b is the separator.
- 7 had `<thead class="bg-slate-50">`, which design.md forbids by name. Removed.
- users/_languages had NO separator at all once the fill went -- and its fill hid
behind a longer class string ("bg-slate-50 text-left text-xs font-semibold
text-slate-600"), so my earlier grep for the exact attribute reported zero
residual fills when one remained. Re-grepped for any thead with a bg-* class.
- reimbursements had divide-slate-100 rows; the token is divide-slate-50.
design.md already named that table as a prior drift site.
all_casa_admins/casa_orgs/show deliberately gets no thead rule: its card has a
title block with border-b, and the invariant is that the title block, the thead
row and a table divide-y sum to exactly one.
Result: 41 of 41 tables satisfy it, 0 fills, 0 tbody off divide-slate-50 -- and the
files changed here are the settings partials (judges, hearing types, contact
types/groups, placement types, languages, custom links, sent emails, learning hour
types/topics), placements, mileage rates, court_dates#show,
emancipation_checklists, both all-CASA tables, hearing_types/_form,
users/_languages and reimbursements.
Verified: spot-checked the affected pages in a browser (org settings alone renders
10 tables) -- all render and all axe clean; spec/system + spec/views pass except
the two known pre-existing SystemStackError view specs. No spec referenced any of
these classes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported as "the typeahead search is not working, the letters the user types stay
even after they have made a selection" on the volunteers page. Both halves are one
bug, and it is not the TomSelect typeahead: the roster's search is a plain field on
a filter bar wired only to `change`.
A text input fires `change` only on blur or Enter, so typing did nothing at all --
measured: typing "Beat" and waiting 2s left the URL unchanged and both volunteers
listed. The text was still sitting in the form, so it applied itself the moment the
user touched any other filter, and stayed in the box afterwards. That is the
"letters stay after a selection" half.
`auto-submit` now searches on debounced `input` (350ms) while selects keep
`change`. Fixed on both bars that have a search field: volunteers and casa_cases.
The awkward part is that submitting rebuilds the page. Turbo Drive is disabled
app-wide (`Turbo.session.drive = false` in application.js), so this is a real page
load, which I confirmed by setting a marker on `window` before the submit and
finding it gone after -- worth knowing, because two of these partials claimed
"Turbo Drive keeps it smooth" and I had believed them. Consequences:
- Module-level state cannot carry the caret. The controller parks
{name, selectionStart, selectionEnd} in sessionStorage and restores it on
connect.
- An immediate focus() is undone as the page settles (measured: activeElement
stayed on body), so the restore is deferred a frame.
Verified by reading document.activeElement and selectionStart rather than by eye:
typing "Beat" with no blur filters to one row, focus is back on the field with the
caret at 4, typing "rice" continues to "Beatrice", and Clear search empties both
the box and the filter. Same on the cases index.
Checked the actual TomSelect typeaheads too, since that is what "typeahead" would
normally mean: the supervisors-index assign control filters correctly and clears
its input on selection. My first probe there appeared to show filtering broken --
that was a race in the probe, not the app; with a wait it renders exactly the one
matching option.
Two of my own spec bugs fixed on the way: the new spec asserted a full row string
including a FactoryBot-sequenced email (email2@ vs email411@ depending on what ran
first), and reached for the clear control with click_link when it only has an
aria-label, which Capybara does not match unless enable_aria_label is set.
spec/system 722 examples clean twice; jest 126/126.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Audited all 13 TomSelect controls (5 single, 8 multi) across 8 pages, driving each
one: type, check it filtered, select, then check the query is gone, a chip
rendered, and the native <select> updated.
The reported bug was real and it is the multiselects. TomSelect does not clear the
search query on select; you have to ask it to. multiple_select_controller has two
init paths and only the grouped one carried
`onItemAdd { setTextboxValue(''); refreshOptions() }`, so every plain multiselect
left the typed letters sitting next to the new chip while all five single-selects
were fine. A/B with the fix stashed: 6 controls report `typed text remained` --
contact types on the case-contacts filter, case_groups#new cases, and all four
report filters -- and 13/13 pass with it.
Four controls initially looked missing or broken and were not. Recording them
because each cost a round trip:
- The court-report case picker lives inside a Dialog, and the case-contact
filters inside a collapsed disclosure panel -- they have to be opened first.
- learning_hours and other_duties build their option list from names already in
the data (@roster_names comes from existing rows), so with no learning hours or
duties the dropdown is legitimately empty.
- emancipation_checklists is volunteer-only (see_emancipation_checklist? =>
is_volunteer?) and redirects an admin to the dashboard. Worth noting: the
whole-app WCAG sweep earlier in this branch recorded that page as "clean" for
an admin -- it had audited the dashboard it was redirected to.
My probes were wrong three times before the audit was trustworthy, all the same
kind of mistake: reading the DOM before the framework had finished. `page.all`
does not wait, so counting wrappers straight after a visit beat TomSelect's init
and reported a working control missing; the first probe read the dropdown before
filtering had run and appeared to show filtering broken; and addressing controls by
`.ts-wrapper` position picked up a control inside a closed dialog. The spec now
locates each control through `select.tomselect.wrapper` and waits for it.
Kept as spec/system/typeahead_controls_spec.rb, which asserts rather than logs and
checks the control count, so a new typeahead has to be added to it.
spec/system 723 examples clean; jest 126/126.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On casa_cases#edit every contact-type option carried a subtext of "never" unless that type had been logged for the case. One correction to the premise: it did not come back. casa_cases#edit has never had the `render_option_subtext: false` that casa_cases#new passes -- `git log -S` on that file returns nothing -- and the multi-select subtext has come from `last_time_used_with_cases` since it was introduced. What happened is that the fix landed on one of two call sites: the contact-type checkboxes got `last_logged_hint_with_cases`, which returns nil for never precisely "so the form can omit the line rather than show a bare never", while the multi-select kept the twin that returns the string. Two methods answering the same question differently. Consolidated onto the one method and deleted the divergent twin, so no caller can reintroduce it. A used type still shows "Last logged 3 days ago"; an unused one shows nothing, which is the point -- "never" repeated down the list is noise, and turning the subtext off wholesale (as new does, because no case exists yet) would have thrown away the useful half. The subtext is `.to_s`, not the bare nil: the option template substitutes it through TomSelect's escape(), which stringifies nil to the literal "null" -- a worse bug than the one being fixed. Verified in a browser on the edit page: used type "Last logged 3 days ago", unused type subtext "", no "never" and no "null" anywhere. Guarded by spec/system/casa_cases/contact_type_options_spec.rb. Also stabilised the typeahead audit added in the previous commit. It was flaky (up to 4 runs in 6) and every cause was in the probe, not the app: a fixed sleep read the menu mid-filter; `.ts-dropdown .option` is not scoped to a control so it picked up a menu left open by the previous one; `currentResults` is not the post-query set at read time; and clicking .ts-control inside a just-opened modal raced the dialog. It now closes stale menus, focuses through the TomSelect instance and types into the focused element, and asserts only the contract that actually regressed -- after a selection the query is cleared and the value registers. Whether the menu narrows was verified by hand across all 13 controls; asserting it from a spec was the flaky part, and that is written down in the spec rather than left implied. 8 consecutive clean runs. spec/system 724 examples clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… grey panel
The court-orders card on casa_cases#edit carried, in order: a heading, the
youth-names note, a **grey bordered panel** holding a labelled select and a Copy
button, the order rows, a divider, and a second labelled select with an Add
button. Two input clusters competing for the same job, and the grey panel is a
filled nested surface inside a card -- card-in-card, which is not a design-system
surface.
Copying every order from a sibling case is an occasional bulk shortcut, not part of
the primary flow, so it now sits behind **one secondary action in the section
header** ("Copy from another case") and the case picker moves inside the Dialog it
already had, alongside the confirmation copy and the inline error. The card body is
then just the order rows and the add row.
Measured in a browser, before -> after: 2 labelled clusters -> 1, tinted panels in
the card -> 0 (the only bg-slate-50 left is the secondary button token itself), one
divider, and axe clean. The rows keep their `rounded-lg border p-3` -- that is the
documented nested sub-form pattern; a *fill* inside a card is the problem, not a
border.
Validation had to move with the picker: opening the dialog cannot validate a choice
that has not been made yet, so `copy-court-orders#confirm` now checks the select and
`#open` just opens. The `caseNumber` target is gone with the "copy from case #X?"
echo -- the user picks the case inside the dialog, so naming it back to them was
redundant.
Nine specs across the admin and volunteer blocks drove the old flow (click Copy with
nothing chosen, select-then-Copy, and the three copy-behaviour tests); all updated to
open the dialog first. Their intent is unchanged -- the validation error, the option
list excluding the current case, and the copy/no-overwrite/no-move behaviour are all
still asserted. The one-case volunteer test now also asserts the trigger is absent by
name rather than by id, and its `have_selector("casa_case_siblings_casa_cases")` was
missing its `#`, so it had been passing against a tag name that never exists.
casa_cases + court_dates + views + axe 177 examples clean; spec/system 724 clean;
jest 126/126.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…u just added
Two things about the row that "Add a court order" creates.
1. Neither field had a real label. The textarea was named by
`placeholder: "Describe the court order"` and the select by its blank option
("Set implementation status"), with aria-labels on both. That passes axe -- there
*is* an accessible name -- which is why it survived the earlier sweep:
placeholder-as-label is not machine-detectable. It still fails the user, because
the placeholder disappears the moment you type, so the field loses its identity
exactly when you are checking what you wrote.
Both now carry a real <label> (verified `for` resolves to the field, in cloned
rows too -- the nested-form controller rewrites NEW_RECORD everywhere, including
the label). Compact token (text-xs slate-500) so a repeated row does not get
heavy; the duplicate placeholder and the aria-labels are gone, the latter because
with a real label they only risk the visible and announced names drifting apart.
The select's blank option is now a neutral "Select a status".
Two bits of geometry, measured rather than eyeballed: the label goes OUTSIDE the
relative wrapper that positions the chevron (inside, `top-1/2` re-centres against
label+select and drops the chevron below the control -- now select mid == chevron
mid, delta 0px), and Delete takes `sm:mt-5` so it lines up with the inputs rather
than the labels above them (label block measures 20px; all three tops now 881px,
and the offset correctly does not apply once the row stacks below sm).
2. Why the fields appear above the button: they append to the end of the list, and
the Add control sits after the collection, so anything else would put the button
in the middle of the list. That is the convention (GOV.UK "add another", Material,
Polaris). What was actually missing is focus -- the upstream controller inserts
the row and leaves focus on the button, so a keyboard user has to go hunting.
court-order-form#add now focuses the new textarea with the caret after any
prefilled standard-order text.
Caught by the specs while doing it: my first attempt used
`.court-order-entry:last-of-type`, which is not jQuery's `:last` -- the rows share a
parent with the insertion target div, so it matched that target, found no textarea
and silently broke the standard-order prefill. It also needed `npm run build:css`:
the new arbitrary margin utility was not in the compiled CSS, so my first alignment
measurement was 20px out and I nearly recorded that as the layout.
Specs updated: two located the textarea by the removed aria-label (now the class
hook the other specs already use), one asserted the old blank-option text.
casa_cases + court_dates + bulk_court_dates 64 clean; axe clean at 1400px and
390px; jest 126/126.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…try box The entry row still had the order textarea, the status select and Delete side by side in a `sm:flex-row` bordered box. design.md "Form layout" already specifies this: wide fields take the full width, and "a status select" is named there as a one-column field. So the order text now spans the full width on its own line and the status sits on the line below with Delete beside it (below `sm` the select goes full width and Delete wraps under it). Measured: textarea 622/622 = 100% of the row, select top below the textarea's bottom, select bottom == Delete bottom. The per-entry bordered box is gone -- that is a card inside the form card. Entries are separated by a hairline (`border-t` + `first:border-t-0`), the same dividers-not-boxes rule as the dashboard worklist. Measured on the rendered page: the entry carries a 1px top border only, no left border, no radius, no fill. Reconciled the design.md entry that licensed this, which is the more useful half of the fix. That section described the opposite layout -- all three controls in a `flex-col sm:flex-row` bordered card -- and I used it as authority to keep the cramped row through two rounds, including one where I measured the three controls into alignment at 881px without asking whether they belonged on one line. It now states the stacked layout, and says plainly that a note in a component entry does not override a rule in a general section: if they disagree, the note is the drift. Also corrected the "rows stay bordered and unfilled" line (an outlined box per entry is still a nested card, not just a fill) and replaced the `sm:mt-5` alignment nudge, which only existed to rescue the one-line row, with the bottom-alignment rule that actually applies now. casa_cases + court_dates + bulk_court_dates + axe 161 examples clean; axe clean at 1400px and 390px; jest 126/126. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e picker searchable Three follow-ups on the same card. 1. Delete now centres on the status field. The fix is structural, not a nudge: the label moves ABOVE the flex line so the line holds only the select and the button, and `items-center` then centres on the control. While the label sat inside the line every alignment was measured against label+select, so the button could never centre on the field -- my two earlier attempts (`sm:mt-5`, then `items-end`) only made one edge agree, and I verified the second with "select bottom == Delete bottom", a measurement that confirms the choice instead of testing it. Now select mid == Delete mid, delta 0px. 2. The rule above the add row is gone. Entries are already separated by a hairline of the same weight, so a second one there read as another entry rather than a section break -- which is why its purpose was unclear. Space separates it now (`mt-6`), as GOV.UK "add another" does. Measured with two entries: entry rules 0px/1px (only between entries), rule above the add row 0px. 3. The dialog's case picker is a searchable-select. An org can hold hundreds of cases, so scrolling a native dropdown to find one is unusable. This app already had the typeahead for exactly this -- I audited all 13 of them the turn before and still reached for a plain <select> while rebuilding this very control. Verified: typing "ZZZ" narrows to ZZZ-9/ZZZ-8 with the AAA-1 decoy gone, the native select's value syncs for the PATCH, and the menu renders inside the dialog. Deliberately no `dropdownParent: body` -- outside the dialog's top layer the menu paints behind it; and only `class: "block w-full"`, since tom-select copies the class onto .ts-wrapper and a bordered input class double-borders. Nine spec sites drove that select with `select ... from:`, which cannot reach the clipped native element once TomSelect owns it, so they share a `pick_case_to_copy` helper that drives it as a user does; the two "options are present" checks read the native select with `visible: :all`. design.md updated for all three, including replacing the `items-end` rule I had written there. casa_cases + court_dates + bulk_court_dates + axe 161 clean; casa_cases/edit 46 clean; axe clean; jest 126/126. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… which
Answering the question first: no, it was not a calendar year -- it was not a year
at all. Neither `supervisor_volunteers_learning_hours` nor
`all_volunteers_learning_hours` filtered `occurred_at`, so "Time completed this
year" was an all-time total. Proven rather than inferred: 1h today + 2h from three
years ago + 4h from Dec 31 came back as 420 minutes, not 60.
So the header named a period the query did not apply, which is worse than no label
because it gets trusted. Now:
- The two aggregate scopes take an optional range (`occurred_in`, a no-op on nil,
so the Pundit scopes keep meaning "everything this user may see" instead of
inheriting a UI default), threaded through LearningHoursDashboardRowsService.
- learning_hours#index parses from/to and defaults to the **calendar year to
date**, which is what the header always claimed.
- The filter bar gains From/To date fields beside the volunteer typeahead, same
tokens as the other rosters, auto-submitting on change. The bar also gains
visible labels, which it did not have.
- The header states the period: "Time completed / since January 1, 2026", or
"X to Y" when the end date is not today.
**This changes the numbers on that page**: they were all-time and are now
year-to-date, so totals will drop for anyone with hours from previous years. That is
the label's stated intent, but it is a product call -- if you would rather the
default stay all-time, it is one line in `set_period`.
Parsed dates are clamped to 1989-01-01..today. `Date.parse` accepts "0730-02-02",
and without the clamp a mistyped param put "since February 2, 0730" in the header --
which my own spec hit, because typing into `<input type=date>` under Selenium is
locale-dependent and turned "2021-07-30" into year 730. The spec now sets the value
via JS, as the other date-field specs here do.
Verified: default shows 1h (the 6h of older hours excluded), widening From to five
years ago shows 7h, and the header follows the range. New
spec/system/learning_hours/date_range_spec.rb covers both plus the clamp. One
request spec asserted the old header text. spec/system + requests + models +
services 2433 examples clean; axe clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There is no convention for it -- it was the only rose-bordered white card in the app. Every comparable section is a plain `border-slate-200` card: volunteers/_manage_active, supervisors/_manage_active, casa_admins#edit and the all-CASA admin edit. Now this one is too; verified its border resolves to the same colour as a sibling card on the same page. It also contradicted a rule design.md already states. The destructive affordance here is deliberately **no red-at-rest**: `button_classes(:danger_outline)` is defined to be identical to `:secondary` at rest and rose only on hover, which the Buttons section calls the industry-standard treatment (GitHub, Gmail, Linear) and spells out as "either way there is no red-at-rest ... the one solid rose is the CONFIRM button in a delete dialog". An always-on rose outline around the section is exactly what that rules out -- so the border was not just unconventional, it undid the reasoning behind the button style. A rose border does have a use: an **alert message** panel, paired with `bg-rose-50` and rose ink. `casa_cases/_inactive_case` is the legitimate one, saying the case *is* inactive. Outlining a section in red to mean "something dangerous is in here" is not that. While confirming the remaining signal was intact I found the trigger had no icon, though "Deactivate volunteer" and "Deactivate supervisor" both use `bi-slash-circle` and design.md names the icon as part of the affordance. Added, so the three read the same. casa_cases + views + axe 160 examples clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lunteer card
Three things, one of which was a real bug.
The hanging colon: "Unassigned:" rendered with an empty <dd> on every *active*
assignment, because the pair was always emitted and only the value was conditional.
An active assignment cannot have an unassigned date, so the pair is now omitted
entirely -- the "Assigned" pill already says the state. The app's muted "Not set"
value stays for fields that genuinely apply but are unfilled (as casa_cases#show
does); this was not one of those.
The typography. The card had 7 size/weight/colour combinations in one row and it is
now 6, but the count was never the problem -- shared roles were. "Enable
reimbursement" (a control) sat at text-xs font-medium text-slate-600 while the fact
*values* were text-xs font-medium text-slate-700: the same thing to the eye, so an
actionable checkbox read as another piece of metadata, which is exactly why the card
was hard to parse. Each role now has one treatment, all of them already in design.md:
identity text-sm font-medium + name_link_class, email the muted/meta text-xs
text-slate-500 (it was text-sm), facts the canonical dt/dd pair (weight on the muted
label, colour on the value -- it had font-medium slate-700 on both, flattening label
against value), and the checkbox the Label token text-sm font-medium text-slate-700.
Two sizes, two weights, colour carrying the role.
Industry standard for this row is a resource-list item -- identity, muted secondary
identity, status badge, label:value metadata, then actions -- and that is the shape
it already had; it just used four inks at two sizes without mapping them to roles.
Measured rather than eyeballed, and both assignment states checked: 1 empty dd -> 0,
and the checkbox label now differs from the fact values in size, weight and colour.
New spec covers the omitted pair (both states) and the control token. One existing
spec asserted the old behaviour -- `find("[data-test=assignment-end]").text` being
empty -- and now asserts the element is absent.
casa_cases + supervisors + axe 198 examples clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… on every assign picker The email and the assignment dates were 12px because I shrank them last turn to cut the number of type sizes in the card, and reported that as an improvement. Reducing style variety was a means and I treated it as the goal: "the volunteer email font looks too small ... very difficult to read". Email and the inline Assigned/Unassigned pair are back at text-sm (measured 14px for email, dt and dd), and only the status pill keeps text-xs. Roles stay one-to-one -- 6 size/weight/colour combinations, each mapping to exactly one role -- because the fix for a flat card is the role mapping (weight and colour), not the size. Same 12px-on-content drift fixed where it had spread: the supervisor card's "Currently assigned to: <name>", the volunteer notes' Creator/Date pairs (both also had the fact tokens backwards -- weight on the value instead of the muted label), and the supervisor dashboard's worklist emails, desktop and mobile. Left alone: the account line in the nav (chrome) and the stacked dt/dd variant, where a 12px label sits above a 14px value on purpose. The assign pickers are now searchable, all of them, not just the one that was reported. "Assign a volunteer to this case" was the ask; its siblings had the same plain <select> over a person or case list -- assign a volunteer to this supervisor, assign a case to this volunteer, assign a supervisor to this volunteer, and assign a volunteer while creating a case. Five were still native a full turn after the rule was written in design.md and after the copy-court-orders picker was converted. Notes on the conversions: - dropdownParent: body on the three inside `overflow-hidden` cards. Measured with the menu open: it extends 125px below the card's bottom edge, which is exactly what an in-flow menu would have been clipped to. - The caret is the shared `.ts-wrapper::after`, pixel-verified against the canonical single-select chevron in the same screenshot: darkest pixel rgb(143,154,171) vs rgb(139,153,172). - `disabled` goes on the <select>, not only the ancestor fieldset: the fieldset makes the control inert but does not set the property TomSelect reads, so the empty state rendered a white, live-looking picker. tailwind.css now styles `.ts-wrapper.disabled` (tom-select ships nothing for it) -- verified slate-100 + input disabled. - volunteers/_manage_supervisor had no blank option, so the first supervisor was pre-selected and clearing a TomSelect without one makes the browser re-select it, which would submit that person silently. It loads blank now, guarded twice (`required` for no-JS, toggle-submit for JS) since `Supervisor.find` raises on a blank id. Two specs asserted the old exact option list. - Still native, deliberately, both recorded in design.md: the bulk assign-supervisor modal (`value=""` means "None" there and the theme hides empty-valued options, so it needs a sentinel change first) and the two volunteer/supervisor filter selects (a filter-bar decision, not a drive-by). The audit spec now drives 18 controls, up from 13. Four existing specs used a bare `.ts-control` that a second control on the page made ambiguous; a new dual-driver helper picks through the native <select> so one call works under rack_test and :js. casa_cases + volunteers + supervisors + dashboard + axe + typeahead audit + views + requests: 703 examples, 2 failures, both pre-existing (mileage_rates/index and casa_admins/admins_table SystemStackError, confirmed by stashing this work). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…able
The last two person-list selects. Both filter over an unbounded list -- every
volunteer with a reimbursement, every supervisor in the chapter -- while the
learning-hours and other-duties volunteer filters have been searchable all along.
Three things a filter picker needs that a form picker does not, all measured in a
browser rather than assumed:
The "All ..." row carries a non-empty value ("all"), not "". The theme hides
empty-valued options in a menu (so a label-less blank option is not an empty row),
so as value="" the row that CLEARS the filter would have been missing from the very
menu the user opens to clear it -- the same trap that keeps the bulk assign-supervisor
modal native, where "" means None. volunteers_controller already read "" and "all"
alike; reimbursements_controller now does too. No placeholder either: a filter states
its current state, including All, exactly as the native select did.
Height matches the bar, not the form. Filter fields are native py-2 selects and date
inputs at 38px while .ts-control's min-height is the 42px form-input height, so the
converted control stood 4px taller than every field beside it -- bottoms aligned by
items-end, tops 4px out. Measured 38 == 38 after `.ts-wrapper.ts-filter .ts-control
{ min-height: 2.375rem }`, tops and bottoms equal on both bars.
No clear x. A filter always has a value, so the wrapper is permanently .has-items and
the clear-button rules swapped its caret for an x on hover/focus while the native
selects beside it kept theirs -- and "clear" on *All supervisors* means nothing. The
reset is the All row.
That last one came out of correcting my own verification. I reported the caret on the
assign pickers as "pixel-verified" on the strength of a darkest-pixel comparison, and
darkest-pixel is only a colour check: on this bar it read as a match while the region
actually held the clear x -- an X, two crossing diagonals at 7x7, because the probe
left the control focused. Comparing the ink bounding box and coverage instead shows a
real match at rest: 10x6 ink, 20 ink pixels, mean lum 251.4 vs 251.2 against a native
chevron in the same screenshot, and the caret survives focus now (rotated, no x).
bin/caret-map.rb is that comparison as a committed tool (stdlib zlib PNG decode -- no
image gem here), and design.md says to compare shape, not the darkest pixel.
Filtering and resetting both verified end to end: pick a volunteer -> 1 row, pick All
volunteers -> 2 rows, and the URL carries volunteers=all / supervisor=all. The audit
spec drives 20 controls now, up from 18.
spec/system + spec/views + spec/requests: 1875 examples, 2 failures, both pre-existing
(mileage_rates/index and casa_admins/admins_table SystemStackError).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR continues the casadesign rebuild follow-ups by aligning Tailwind UI patterns across multiple pages, improving roster/filter UX (typeahead + search-as-you-type + drill-through return paths), tightening authorization/tenancy correctness, and stabilizing/expanding system coverage (including accessibility and JS behaviors).
Changes:
- Add/standardize Stimulus behaviors: auto-dismiss success flashes, debounced auto-submit search with caret restore, and TomSelect query-clearing on multi-select.
- Improve navigation/UX flows: supervisors→volunteers drill-through with preserved return path, more consistent table/card layouts across dashboards and rosters, and searchable-select pickers for large person/case lists.
- Fix correctness/security issues: org-scoped mileage rate access + dedicated policy, and switch authorization failures to
flash[:alert]to avoid rendering as success/auto-dismissing.
Reviewed changes
Copilot reviewed 134 out of 135 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/views/supervisors/index.html.erb_spec.rb | Updates view expectations for supervisors roster numeric stats rendering. |
| spec/views/placements/new.html.erb_spec.rb | Adjusts selector expectations after placement subtitle markup change. |
| spec/views/court_dates/new.html.erb_spec.rb | Adjusts selector expectations after court date subtitle markup change. |
| spec/system/volunteers/search_spec.rb | New system coverage for debounced search-as-you-type + caret/focus preservation. |
| spec/system/volunteers/index_spec.rb | Adds system coverage for supervisors→volunteers drill-through return path behavior. |
| spec/system/volunteers/edit_spec.rb | Updates specs for new searchable-select blank option and honorific-stripping display. |
| spec/system/typeahead_controls_spec.rb | New “audit” spec exercising TomSelect controls and query-clearing behavior. |
| spec/system/supervisors/index_spec.rb | Adds drill-through coverage; updates zero-volunteer display expectations. |
| spec/system/reimbursements/reimbursements_spec.rb | Updates volunteer filtering spec for new typeahead picker helper. |
| spec/system/placements/edit_spec.rb | Adds a stronger page-anchor assertion to reduce navigation race flake. |
| spec/system/other_duties/new_spec.rb | Switches to Capybara-synchronized validation message matcher to avoid stale nodes. |
| spec/system/learning_hours/date_range_spec.rb | New system coverage for learning-hours date range behavior/clamping. |
| spec/system/layouts/flashes_spec.rb | New system coverage for success auto-dismiss and error persistence. |
| spec/system/dashboard/show_spec.rb | Adds system coverage asserting the “Needs your attention” section renders as a table. |
| spec/system/court_dates/view_spec.rb | Updates XPaths after heading/definition term markup changes. |
| spec/system/court_dates/new_spec.rb | Removes raw Selenium element caching; uses stable Capybara find/set for nested rows. |
| spec/system/case_contacts/edit_spec.rb | Adds system coverage that autosave persists changes across more control types. |
| spec/system/casa_cases/volunteer_assignment_card_spec.rb | New system coverage for volunteer assignment card content/typography/typeahead. |
| spec/system/casa_cases/new_spec.rb | Updates selectors and uses typeahead helper for assign-volunteer picker. |
| spec/system/casa_cases/edit_spec.rb | Updates copy-court-orders flow to use dialog + searchable-select; stabilizes async assertions. |
| spec/system/casa_cases/contact_type_options_spec.rb | New coverage ensuring contact-type recency subtext behavior doesn’t render “null/never”. |
| spec/system/bulk_court_dates/new_spec.rb | Removes stale .native handle usage; adds navigation anchoring to prevent races. |
| spec/system/banners/new_spec.rb | Scopes banner list assertions after status becomes a pill rather than inline text. |
| spec/system/banners/activate_spec.rb | New system coverage for inactive-banner messaging and list activation behavior. |
| spec/system/accessibility/axe_spec.rb | Expands axe coverage (including mobile viewport) and seeds required data to surface DOM issues. |
| spec/support/typeahead_helpers.rb | Adds helper to drive native vs TomSelect selects consistently across drivers. |
| spec/support/capybara_transient_node_retry.rb | Adds Capybara retry support for a specific transient Chrome detached-node error. |
| spec/requests/volunteers_spec.rb | Aligns request-spec expectations with new blank option and alert flash semantics. |
| spec/requests/supervisors_spec.rb | Switches authorization flash expectations from :notice to :alert. |
| spec/requests/mileage_reports_spec.rb | Switches authorization flash expectations from :notice to :alert. |
| spec/requests/mileage_rates_spec.rb | Adds org-scoping coverage and edit/update authorization correctness tests. |
| spec/requests/learning_hours_spec.rb | Updates header assertions to match date-range-aware “since …” copy. |
| spec/requests/judges_spec.rb | Switches authorization flash expectations from :notice to :alert. |
| spec/requests/hearing_types_spec.rb | Switches authorization flash expectations from :notice to :alert. |
| spec/requests/followup_reports_spec.rb | Switches authorization flash expectations from :notice to :alert. |
| spec/requests/custom_org_links_spec.rb | Switches authorization flash expectations from :notice to :alert. |
| spec/requests/court_dates_spec.rb | Switches authorization flash expectations from :notice to :alert. |
| spec/requests/contact_types_spec.rb | Switches authorization flash expectations from :notice to :alert. |
| spec/requests/contact_type_groups_spec.rb | Switches authorization flash expectations from :notice to :alert. |
| spec/requests/checklist_items_spec.rb | Switches authorization flash expectations from :notice to :alert. |
| spec/requests/case_assignments_spec.rb | Switches authorization flash expectations from :notice to :alert. |
| spec/requests/casa_cases_spec.rb | Switches authorization flash expectations from :notice to :alert. |
| spec/requests/casa_admins_spec.rb | Switches authorization flash expectations from :notice to :alert. |
| spec/decorators/contact_type_decorator_spec.rb | Updates expectations for blank subtext when no contacts exist. |
| spec/controllers/emancipations_controller_spec.rb | Switches authorization flash expectations from :notice to :alert. |
| design-todo.md | Adds Part 2 tracking checklist items and rationale notes. |
| bin/caret-map.rb | Adds a utility to pixel-compare carets/chevrons in screenshots (design verification support). |
| app/views/volunteers/index.html.erb | Adds supervisors drill-through origin handling and preserves it into volunteer links. |
| app/views/volunteers/edit.html.erb | Preserves drill-through origin when navigating back to volunteers list. |
| app/views/volunteers/_notes.html.erb | Aligns fact-pair styling to design tokens; tweaks table header row styling. |
| app/views/volunteers/_manage_supervisor.erb | Converts supervisor assignment to searchable-select with blank option + validation hint. |
| app/views/volunteers/_manage_cases.erb | Converts case assignment select to searchable-select (with body dropdown parent). |
| app/views/volunteers/_filter.html.erb | Adds debounced search-as-you-type + drill-through preservation + searchable supervisor filter. |
| app/views/users/_languages.html.erb | Aligns table header/body styling to updated Tailwind conventions. |
| app/views/supervisors/index.html.erb | Reworks supervisors roster to numeric columns, adds row drill-through action, adds mobile cards. |
| app/views/supervisors/_manage_volunteers.html.erb | Aligns inline fact pair token usage; makes assign-volunteer picker searchable-select. |
| app/views/shared/_flashes.html.erb | Adds auto-dismiss only for success notices; keeps alerts/errors persistent. |
| app/views/shared/_additional_expense_form.html.erb | Removes per-field autosave hook now that autosave binds at the form level. |
| app/views/reimbursements/index.html.erb | Converts volunteer filter to searchable-select; adjusts divider styling. |
| app/views/placements/index.html.erb | Updates link styling and table structure to match new table conventions. |
| app/views/placements/_form.html.erb | Changes case-number subtitle from heading to paragraph for heading-order accessibility. |
| app/views/mileage_rates/index.html.erb | Aligns mileage rates table styling to updated table conventions. |
| app/views/learning_hours/_supervisor_admin_learning_hours.html.erb | Adds date range filter controls and correct “Time completed” header period labeling. |
| app/views/layouts/casa_auth.html.erb | Adjusts auth layout hero headline markup to avoid heading structure issues. |
| app/views/layouts/_casa_banner.html.erb | Fixes banner dismiss control semantics (button) and icon contrast. |
| app/views/imports/_volunteers.html.erb | Adds explicit file-field labels for accessibility. |
| app/views/imports/_supervisors.html.erb | Adds explicit file-field labels for accessibility. |
| app/views/imports/_cases.html.erb | Adds explicit file-field labels for accessibility. |
| app/views/hearing_types/_form.html.erb | Aligns checklist items table styling to updated table conventions. |
| app/views/emancipations/show.html.erb | Fixes heading semantics and adds aria-labels to unlabeled inputs. |
| app/views/emancipation_checklists/index.html.erb | Aligns checklist table styling to updated table conventions. |
| app/views/devise/sessions/new.html.erb | Fixes heading structure (uses h1). |
| app/views/devise/passwords/new.html.erb | Fixes heading structure (uses h1). |
| app/views/devise/passwords/edit.html.erb | Fixes heading structure (uses h1). |
| app/views/devise/invitations/new.html.erb | Fixes heading structure (uses h1). |
| app/views/devise/invitations/edit.html.erb | Fixes heading structure (uses h1). |
| app/views/devise/confirmations/new.html.erb | Fixes heading structure (uses h1). |
| app/views/dashboard/volunteer.html.erb | Converts “Needs your attention” to table/list responsive pattern; aligns actions to ghost buttons. |
| app/views/dashboard/supervisor.html.erb | Same worklist table/list responsive pattern; reduces duplicate row actions. |
| app/views/dashboard/admin.html.erb | Adds next court date column and table/list responsive pattern for unassigned cases. |
| app/views/court_dates/show.html.erb | Fixes dt/dd semantics and court orders heading level; updates table styling. |
| app/views/court_dates/_form.html.erb | Changes case-number subtitle from heading to paragraph for heading-order accessibility. |
| app/views/case_contacts/form/details.html.erb | Moves autosave trigger to form-level input event; removes per-field triggers. |
| app/views/case_contacts/form/_contact_topic_answer.html.erb | Removes per-field autosave hook now that autosave binds at the form level. |
| app/views/case_contacts/drafts.html.erb | Passes heading level into partial to preserve correct heading order. |
| app/views/case_contacts/case_contacts_new_design/index.html.erb | Adjusts text colors for contrast (emerald/amber). |
| app/views/case_contacts/_case_contact.html.erb | Makes heading level configurable to fix heading-order across contexts. |
| app/views/casa_org/edit.html.erb | Improves mobile group label contrast. |
| app/views/casa_org/_sent_emails.html.erb | Aligns sent emails table styling to updated conventions. |
| app/views/casa_org/_placement_types.html.erb | Aligns placement types table styling to updated conventions. |
| app/views/casa_org/_learning_hour_types.html.erb | Aligns learning hour types table styling to updated conventions. |
| app/views/casa_org/_learning_hour_topics.html.erb | Aligns learning hour topics table styling to updated conventions. |
| app/views/casa_org/_languages.html.erb | Aligns languages table styling to updated conventions. |
| app/views/casa_org/_judges.html.erb | Aligns judges table styling to updated conventions. |
| app/views/casa_org/_hearing_types.html.erb | Aligns hearing types table styling to updated conventions. |
| app/views/casa_org/_custom_org_links.html.erb | Aligns custom org links table styling to updated conventions. |
| app/views/casa_org/_contact_types.html.erb | Aligns contact types table styling to updated conventions. |
| app/views/casa_org/_contact_type_groups.html.erb | Aligns contact type groups table styling to updated conventions. |
| app/views/casa_org/_contact_topics.html.erb | Adjusts active/inactive label colors for contrast. |
| app/views/casa_cases/new.html.erb | Converts assign-volunteer picker to searchable-select. |
| app/views/casa_cases/edit.html.erb | Normalizes deactivate section styling and adds icon for destructive trigger. |
| app/views/casa_cases/_volunteer_assignment.html.erb | Improves readability of facts and converts volunteer picker to searchable-select; avoids empty “Unassigned:” rendering. |
| app/views/casa_cases/_placements.html.erb | Updates “See all placements” link styling to match record link token. |
| app/views/casa_cases/_filter.html.erb | Adds debounced search-as-you-type to match other filter bars. |
| app/views/casa_cases/_court_orders.html.erb | Moves copy-from-sibling into dialog with searchable-select; simplifies list layout. |
| app/views/casa_cases/_court_order_fields.html.erb | Restructures court order fields layout to match form layout tokens; improves labels/prompts. |
| app/views/bulk_court_dates/new.html.erb | Changes subtitle from heading to paragraph for heading-order accessibility. |
| app/views/banners/index.html.erb | Adds status pills and in-list activation action; updates table styling. |
| app/views/banners/_form.html.erb | Adds ARIA label for ActionText editor to satisfy accessibility checks. |
| app/views/analytics/index.html.erb | Adjusts delta text colors for contrast. |
| app/views/all_casa_admins/sessions/new.html.erb | Fixes heading structure (uses h1). |
| app/views/all_casa_admins/dashboard/show.html.erb | Aligns organizations table styling to updated conventions. |
| app/views/all_casa_admins/casa_orgs/show.html.erb | Aligns org users table styling to updated conventions. |
| app/services/learning_hours_dashboard_rows_service.rb | Adds optional date range to aggregated learning-hours roster queries. |
| app/services/admin_dashboard.rb | Adds grouped query for next upcoming court date per unassigned case. |
| app/policies/mileage_rate_policy.rb | Adds policy enforcing same-org admin access for mileage rates. |
| app/models/learning_hour.rb | Adds optional occurred_at range scope and threads it into aggregate queries. |
| app/javascript/controllers/multiple_select_controller.js | Clears TomSelect query on item add for plain multiselects. |
| app/javascript/controllers/index.js | Registers new auto-dismiss controller. |
| app/javascript/controllers/court_order_form_controller.js | Fixes last-row targeting and improves focus/caret behavior after adding court orders. |
| app/javascript/controllers/copy_court_orders_controller.js | Updates copy flow to validate on confirm with dialog-based picker. |
| app/javascript/controllers/auto_submit_controller.js | Adds debounced search handler + caret/focus preservation across full reloads. |
| app/javascript/controllers/auto_dismiss_controller.js | New controller to auto-hide success flashes with pause-on-hover/focus support. |
| app/javascript/tests/case_emancipations.test.js | Updates test DOM to match heading tag change in emancipation UI. |
| app/helpers/metrics_helper.rb | Makes scrollable metric tables focusable (tabindex=0) for accessibility. |
| app/decorators/contact_type_decorator.rb | Unifies recency subtext behavior and ensures TomSelect subtext never becomes "null". |
| app/controllers/reimbursements_controller.rb | Treats "all" volunteer value as no-filter to support TomSelect menu behavior. |
| app/controllers/mileage_rates_controller.rb | Scopes record lookup to org and authorizes per-record using new policy. |
| app/controllers/learning_hours_controller.rb | Adds date range parsing/clamping and passes range into aggregation service. |
| app/controllers/court_dates_controller.rb | Switches unauthorized redirect flash to alert. |
| app/controllers/casa_cases_controller.rb | Switches not-found/unauthorized redirect flash to alert. |
| app/controllers/banners_controller.rb | Adds contextual flash messaging depending on whether banner is active. |
| app/controllers/application_controller.rb | Switches authorization failure flash from notice to alert to avoid success styling/auto-dismiss. |
| app/assets/stylesheets/tailwind.css | Enhances TomSelect/filter-bar styling and disables clear-x for filter selects; wraps Trix toolbar to avoid scrollable-region issues. |
| <%# Volunteer counts, computed once: no_attempt_for_two_weeks walks every volunteer's contacts, | ||
| and the desktop table and the mobile card list below both need them. active_volunteers counts | ||
| only active volunteers while no_attempt_for_two_weeks counts all of them, so the difference can | ||
| go negative -- clamp it, as the pill version did. %> | ||
| <% supervisor_stats = @supervisors.map do |supervisor| | ||
| not_attempting = supervisor.no_attempt_for_two_weeks | ||
| total = supervisor.active_volunteers | ||
| [supervisor, {total: total, attempting: [total - not_attempting, 0].max, | ||
| not_attempting: not_attempting, | ||
| transition: supervisor.volunteers_serving_transition_aged_youth}] | ||
| end %> |
| find("#from").execute_script("this.value = arguments[0]; this.dispatchEvent(new Event('change', {bubbles: true}))", 5.years.ago.to_date.strftime("%Y-%m-%d")) | ||
| expect(page).to have_css("table tbody tr") | ||
| sleep 1 | ||
| expect(find("table tbody tr td:last-child")).to have_text("7 hours") | ||
| expect(find("table thead tr th", text: "Time completed")).to have_text("since #{I18n.l(5.years.ago.to_date, format: :full)}") | ||
| expect(page.current_url).to include("from=#{5.years.ago.to_date}") |
| find(".ts-dropdown .option", text: expect_option, match: :first).click | ||
| page.has_css?("##{id} .ts-control .item", wait: 3) | ||
|
|
|
Hi @giacoelho I just wanted to let you know that this PR introduced a few errors that is impacting other work... is it part of the work to address these errors in a follow up PR? Thank you! |
|
@stefannibrasil @seanmarcia I may be missing context, but is there any reason why we can't revert this PR? The title seems to suggest this is not ready for main yet |
|
It was merged to let the stakeholders do usability testing on the design. |
|
@stefannibrasil @seanmarcia @giacoelho Ah I see. Thank you for that bit of context. Well I definitely don't want to step on any one's toes, but I'm more than happy to help out in any way if needed 🙏🏽 |
open part 2 with the follow-ups carried over from #7051
Starting point for the part-2 tracking PR. Five items, each verified still open against main rather than copied forward from notes:
Dropped one item while checking: the read-notification icon is already slate-500, so its contrast was fixed in the merged work and is not outstanding.