Casadesign - DRAFT DO NOT MERGE - #7051
Merged
Merged
Conversation
giacoelho
requested review from
FireLemons,
compwron and
elasticspoon
as code owners
July 9, 2026 23:01
giacoelho
marked this pull request as draft
July 9, 2026 23:01
…p (Phase 5 batch 3) Hearing-type form: name + Active? in a card, plus (once persisted) a Tailwind checklist-items table (id checklist_items) with New List Item / Edit links and a button_to Delete. The delete uses turbo_confirm only, not the Dialog confirm-button, because a non-JS destroy spec drives "Delete" directly (rack_test ignores turbo_confirm and submits). checklist_items new/edit is a custom casa_app form (Category / Description / Mandatory) nested under the hearing type, redirecting to the hearing-type edit page. custom org links is a casa_app form (Display text / URL / Active?; Submit label from the `action` local). All three controllers get layout "casa_app" + @active_nav = "settings". Dropped the title-wrapper/card-style Bootstrap markup, lni icons, and shared/error_messages (now shared/form_errors). 57 system + view + request examples green; no page overflow (the checklist table scrolls within its card on mobile, like the settings admin tables); standardrb + erb_lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ch 4) Mileage rates: casa_app settings index (date / amount / Active? / Edit, no delete) + a date+currency+Active? form. Placements (nested under a case): casa_app index whose per-row Delete is a Dialog with a visible "Close" + button_to "Confirm", so the non-:js destroy spec passes under rack_test (which can't match the header X's aria-label); a date + chevron-select form and a fact-card show. The index/new view specs move off the Bootstrap .modal / .primary-btn assertions. Banners: casa_app #banners table (keeps td.min-width for the system spec) with a shared/_confirm_button Delete, and a form with the rich-text content editor + a live reveal warning (new reveal Stimulus controller) when another banner is already active. Trix needed its stylesheet folded into the casa_app tailwind bundle (@import "trix/dist/trix.css") — without it the toolbar was unstyled and overflowed the page to ~900px on every width. 58 system + view + request examples green; chevron darkest-pixel 114; pages fit 375-1280 (tables + the Trix toolbar scroll within their own containers). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both nested court-date forms now render on the casa_app shell, sharing court_dates/_fields (date + court-report-due-date + judge/hearing-type chevron-selects) and the existing casa_app court-orders twin (casa_cases/_court_orders, with siblings off + resource "court_date"), so the court-order-form nested sub-form and its dialogs are shared with the case-edit page rather than restyling the Bootstrap shared/_court_order_list. The submit stays wrapped in .top-page-actions (system specs click "Create" / "Update" within it). The show page is a Tailwind card that preserves the <dt><h6>Label:</h6></dt><dd> structure the xpath view/system specs assert (the Title-Case colons ride the deferred sentence-case sweep), and deletes a future court date via UJS method: :delete + data-confirm (native window.confirm, the proven casa_app pattern the volunteer deactivate uses), which the :js spec drives with accept_alert. Court-date view specs move off .primary-btn; the edit spec's court-order heading assertion matches the twin's sentence-case text. 80 system + view + request examples green; chevron darkest-pixel 114; pages fit 375-1280. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Bootstrap JS tabs become server-side link tabs keyed on ?import_type= (the controller already reads @import_type); each tab keeps its id (a system spec clicks "supervisor-tab") and "Import X" label, and only the active panel renders. The CSV error becomes an inline alert instead of a Bootstrap modal. The SMS opt-in step is now an inline amber panel that preserves id="smsOptIn", the sms-opt-in-checkbox / sms-opt-in-continue-button ids, and the enable-on-check script — so the global src/import.js (which enables the import button on file-select and persists the CSV to localStorage so it survives the opt-in reload, gated on #smsOptIn) keeps working unchanged. Import buttons keep their disabled state + ids so import.js re-enables them. 21 system + request examples green; page fits 375-1280. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The emancipation checklist show page moves to the casa_app shell with every
AJAX/collapse hook from the global src/case_emancipation.js preserved verbatim:
the .emancipation-category h6 with data-is-open, its child .category-collapse-icon,
the immediately-adjacent .category-options sibling (the Toggler reads it via
.next()), the .emacipation-category-input-label-pair div (kept a div, not a
label, so it doesn't double-toggle the checkbox the JS flips), each .check-item
with its O#{id} / C#{category} inputs, and a #notifications element so the
Notifier — and thus the whole controller's DOM-ready binding — constructs.
The checklist index becomes a plain Tailwind table; dropping the
all-case-emancipations id makes the global src/emancipations.js DataTable no-op.
42 system + view + request examples green; pages fit 375-1280. This completes
the Phase 5 admin long-tail CRUD migration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…se 6a) Adds a dedicated Tailwind `all_casa_admin` layout (its own sidebar — CASA organizations / Patch Notes / Edit Profile / Feature Flags / Pg Dashboard + Log Out — a simplified top bar, the Ruby For Good / site-issue / SMS-terms footer, and inline flash that keeps a .header-flash hook), set on the base AllCasaAdminsController. Patch notes stays on the legacy `application` layout until its own sub-batch. Migrated the dashboard (org table), casa_orgs new/show (+ the metrics list), and casa_admins new/edit/_form to Tailwind cards, tables, forms, and button_classes. The profile edit's change-password panel now uses the disclosure Stimulus controller (Tailwind hidden toggle) like the regular user edit-profile page, keeping id="collapseOne"; its view + :js specs move off the Bootstrap .collapse/.show classes to .hidden. shared/error_messages stays where specs lock #error_explanation + the "N errors prohibited" wording. 89 all-CASA-admin examples green. The one remaining red (all_casa_admin_spec:14, a casa_admin visiting "/" expecting /supervisors) is a pre-existing regular-app routing failure that fails identically on the legacy views. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuilds all_casa_admins/sessions/new and passwords/new on the Tailwind casa_auth split-screen shell to match the already-migrated devise/sessions/new, replacing the leftover Bootstrap markup. Preserves the spec-locked bits: the "All CASA Log In" heading, the Email/Password labels, the .actions wrapper around "Log in", and the flash block (sign-out lands on this page with "Signed out successfully"). 13 all-CASA-admin session + password examples green; both pages fit 375-1280. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The patch-notes page is a jQuery clone-CRUD (src/all_casa_admin/patch_notes.js
reads the DOM by structure), so the index + _patch_note partial keep the exact
hooks — #patch-note-list's child order (title, the new-note item the JS clones,
then existing notes), each .card-body's direct children (textarea, two
.label-and-select selects, .patch-note-button-controls), and the
.button-edit/.button-delete hooks — restyled to Tailwind cards. Selects keep the
native arrow (the design-system chevron needs a wrapper the JS's
.children('select') can't tolerate).
Two wiring fixes were needed: the shell now also loads the separate
`all_casa_admin` JS bundle (its `tables` DataTable no-ops on the plain Tailwind
tables), and the page renders layouts/components/_notifier because the Notifier
appends into its `.messages` container. The JS's injected Edit/Delete/Save/Cancel
buttons move from Bootstrap + FontAwesome to the button_classes strings + bi-*
icons. Also restores the emancipation show page's notifier (it had the same bare
#notifications, silently dropping AJAX save feedback).
The all-CASA-admin authenticated area is now fully on the design-system shell.
23 patch_notes + 91 all-casa examples green (only the pre-existing
all_casa_admin_spec:14 red); page fits 375-1280.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
static#index (the layout-false marketing page) is rebuilt on the CASA design system — compiled tailwind.css + Figtree + the brand palette, a brand-gradient hero, feature cards, testimonials, org logos, and a contact section — replacing the Tailwind CDN + Alpine + hand-mixed blue/green utilities. Preserves the spec-locked bits: the #organizations section, the .org_logo images, and the "CASA Organizations Powered by Our App" heading. Signed-in users are still redirected away by the controller. 4 examples green; the page fits 375-1280 (the header's decorative blur is clipped by overflow-hidden, like the casa_auth aside). This completes Phase 6: the all-CASA-admin area (shell + org management + auth + patch notes) and the public landing page are all on the design system. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 11 casa_org/edit settings tables (judges, languages, placement/ learning-hour types + topics, contact types/groups/topics, hearing types, custom org links, sent emails) and the hearing-type checklist_items table now render the design-system table twin: the `<table>` is `hidden md:block` and a `md:hidden` list of compact `<dl>` cards serves phones, matching the documented responsive pattern in design.md. Eliminates the horizontal scroll-within-card the admin tables had on mobile. - `<tr id>` and per-row `shared/_confirm_button` stay on the table only; the card omits the id (it's a `<dl>`, not a row) and a duplicate confirm_button is safe (each Dialog is scoped by its own modal controller, id nil). - Card labels mirror the existing table headers verbatim so the Title Case sweep still covers table + card together in one pass. - Emancipation-checklists index (2 columns, measured W360/375) keeps its plain table — it never scrolled. Verified: casa_orgs edit view spec (have_table), casa_org + hearing_types + contact_types system specs, and the resource request specs all green; erb_lint clean; pages measured fitting at 375/414/768/1024/1280. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The completed Phase 6 section above already covers the all-CASA-admin shell, Devise edges, and static pages, so the leftover unchecked stub was duplicated and misleading. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The last Bootstrap table in the case area (feature flag :new_case_contact_table) is now a server-rendered casa_app page, retiring its jQuery serverSide DataTable — matching the cases-index reference. - Server-side filtering reuses the existing CaseContact scopes the DataTable used (contact type as an IN-subquery so multi-type rows don't duplicate), plus ?sort= and Pagy. Filter is a `disclosure` panel that auto-submits (GET); the case picker is the reused `multiple-select` TomSelect (chevron darkest-pixel verified to match the single-selects). - Rows expand (one `<tbody data-controller="disclosure">` per row) to their topic answers + notes; per-row actions are inline icon buttons + native Dialogs (delete confirm, set-reminder note) — no dropdown, which `overflow-x-auto` would clip. - Dropped the DataTables column-visibility picker (design already rejected it for the cases index; the md:hidden card stack covers mobile). - Reminders stay on the list: followups#create/#resolve now redirect_back_or_to (destroy already redirects to the referer). Added a `wrapper_class` passthrough to shared/_confirm_button for menu/inline reuse. - Drops the id="case_contacts" hook so dashboard.js's defineCaseContactsTable no-ops; the now-dead datatable action + CaseContactDatatable + JS block are queued for cleanup. 38 request (incl. new server-side filter examples) + 22 system examples green; the concern spec updated for the migrated controller. Documented the expandable-rows + inline-actions pattern in design.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The migrated new-design page is server-rendered, so its jQuery serverSide DataTable and JSON endpoint are unused. Removes them: - `CaseContacts::CaseContactsNewDesignController#datatable` action + its `datatable_case_contacts_new_design` route. - `CaseContactDatatable` (+ its spec) — no other consumer; the sibling Volunteer/Supervisor/Reimbursement datatables + ApplicationDatatable stay. - The `defineCaseContactsTable` block in `dashboard.js` (its DataTable init, row handlers, `collectCaseContactFilters`, the `buildTopicPills` / `buildExpandedContent` helpers, and the now-unused `Swal` / `fireSwalFollowupAlert` imports) + its call site + export. Also drops the stale `table#case_contacts` branch from the notifier guard. The kept volunteers/supervisors/casaCases tables + `pageNotifier` are untouched. - `dashboard.test.js` (entirely `defineCaseContactsTable`) and the `POST /datatable` request-spec block. Verified: jest 129/129, standardjs + standardrb clean, esbuild build compiles, new_design request 11/11 + system 22/22. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auditing controllers off the casa_app/casa_auth/all_casa/metrics layouts surfaced several reachable leaf pages never in the phase plan that are still on the Bootstrap `application` layout (fund_requests#new, casa_admins, other_duties, notes#edit, case_assignments#index, error pages) — they block deleting the Bootstrap layout, so they're now a tracked section and the "resume here" pointer. Also closed the Font Awesome sweep (0 fas fa-* left in casa_app views) and flipped the case-contacts index item to done. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First of the Bootstrap-layout stragglers. Rebuilds the New Fund Request form on `layout "casa_app"` (@active_nav = "cases", back-link to the case), reusing the casa_cases/new form pattern: max-w-2xl card, a 2-col grid of the short fields + full-width textareas, shared/form_errors, and a single bottom primary submit. - Kept the field-label substrings the system spec fills by, and the * / ** footnotes explaining the documentation/payee notes. Only submitter_email is required (model validation); prefills (email / youth / requested-by) now use `.presence || default` so user input persists on a validation re-render. - Sentence-cased the feature copy per the design rule (h1 + page title, the "Submit fund request" button, the success flash, and the case-show More link), updating the two coupled spec assertions. 2 system + 11 request examples green; standardrb + erb_lint clean; no horizontal overflow at 500 / 768 / 1280. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second straggler batch. Rebuilds the CASA-admin roster on `layout "casa_app"`
as self-contained views mirroring the supervisors sibling (retiring the shared
Bootstrap casa_admins/_form):
- index: a bespoke Tailwind table (email + Edit, "Deactivated" tag) with the
#admins spec hook on the wrapper div.
- new: a minimal casa_app form (email / display name / phone), invitation on save.
- edit: Profile form (email / display name / phone / DOB + monthly-report
checkbox) + an Account facts panel + a Status section. Deactivate keeps the
UJS data-confirm (native window.confirm) the :js spec drives via accept_confirm;
Resend Invitation / Change to Supervisor stay UJS data-method links.
Kept the spec-locked legacy error format ("N errors prohibited this Casa admin
from being saved:" + #error_explanation) via a casa_app-styled casa_admins/_errors
partial. This retires casa_admins/_form and leaves shared/_edit_form +
shared/_invite_login without a consumer (queued in the dead-code cleanup list).
Spec-locked Title Case labels kept verbatim and queued for the sentence-case sweep.
53 casa_admins examples green (index/new/edit incl. :js + request); standardrb +
erb_lint clean; no horizontal overflow at 500 / 768 / 1280.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Third straggler batch. Rebuilds volunteer "other duties" logging on
`layout "casa_app"`:
- index: a bespoke Tailwind table grouped by volunteer (desktop table + a mobile
card twin), keeping the volunteer-only "New Duty" link.
- new/edit: unchanged thin `render "form"` wrappers over a rebuilt casa_app
`_form` — the Hour(s)/Minute(s) duration inputs are the Tailwind twin (like
learning hours), and the controller's convert_duration_minutes still combines
them into duration_minutes. Notes keeps `required` + the default
#other_duty_notes id (a :js spec asserts the HTML5 validation message).
Swapped shared/error_messages for shared/form_errors (no spec locks the legacy
"N errors" format here). Spec-locked Title Case labels ("Other Duties", "New
Duty", "Editing Duty", "Occurred On", "Duty Duration", "Enter Notes") kept
verbatim and queued for the sentence-case sweep.
26 examples green (view + system + request); standardrb + erb_lint clean; no
horizontal overflow at 500 / 768 / 1280.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fourth straggler. Rebuilds the single volunteer-note edit form on `layout "casa_app"` (@active_nav = "volunteers"): a max-w-2xl card with a back-link to the volunteer edit page it's reached from. Note has no validations, so update always redirects back — no failure re-render, no error partial needed. Kept note[content] + the spec-locked "Update Note" button (queued for the sweep). 23 examples green (system + request); standardrb + erb_lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The straggler audit flagged case_assignments#index, but it isn't a page: `resources :case_assignments, only: %i[create destroy]` defines no index route or action, and the view referenced an @volunteer nothing assigns (it even used the DataTable id='casa_cases'). The view could never render, so delete it rather than migrate. case_assignments_path is only the create POST target, unaffected. 45 case_assignments request examples green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…2/500) The public/ error pages were on the old Rails-default styling (gray #EFEFEF dialog boxes, #00447c headers, Arial). Rewrite all six to match the design system: slate-50 surface, a centered white card with a rounded-2xl slate border + indigo brand-gradient accent, the Figtree fallback stack, an "Error NNN" eyebrow + slate-900 heading + message, and a brand "Back to homepage" button with a "Report a site issue" link where useful. Kept fully self-contained (inline hardcoded hex) since they're served straight from public/, even when Rails is down. Per page: 403 (not authorized) shows only Back to homepage; 406 (unsupported browser) gives browser guidance and no action button (switching browsers is the fix); 400/422/500 keep the "application owner, check the logs" note. Verified via CDP: all six render slate-50 bg, 16px card, brand-600 button, the right heading + actions per page, 0 console errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a small on-brand illustration — the CASA butterfly (public/butterfly.svg, white) on an indigo brand-gradient circle badge with a gentle float (disabled under prefers-reduced-motion) — above the status on all six static error pages, for a touch of warmth. Copy stays neutral/professional per page (no jokes): "Page not found", "Something went wrong", etc., so the delight scales with the low stakes of an error page without being flippant when something actually broke. Verified via CDP: the butterfly loads and the gradient badge renders on all six, the neutral headings + per-page actions are unchanged, 0 console errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… button partial Rework layouts/mailer.html.erb from the old gray-header table (#728089 header, #f6f6f6 field, muted 13px body) to the design system: slate-50 canvas, a brand wordmark, a white rounded card with an indigo (#4f46e5) top accent + slate-200 border, slate-700 body type, and a slate footer with the org name + address. All inline + table-based per ADR 0007 (border-radius degrades to square in Outlook; the solid accent bar is Outlook-safe). Add app/views/mailer_shared/_cta_button.html.erb — a bulletproof (bgcolor td + padded anchor) brand-600 button — so the per-template CTAs stop using the old #348eda blue. Templates are converted to the new type/button in follow-up commits; verified the layout renders via the mailer preview. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er layout Devise's mailer used ActionMailer::Base (parent_mailer was commented out), so its emails skipped layouts/mailer entirely — reset_password was a full standalone doc with the old #00447c header, and confirmation / password_change / email_changed / unlock were bare, unstyled <p> fragments. Set config.parent_mailer = "ApplicationMailer" so every Devise email uses the shared design-system layout, and rewrite the six HTML templates as clean fragments (slate body paragraphs + the brand cta_button partial): invitation, reset password, confirmation, password change, email changed, unlock. Copy preserved where spec-locked (e.g. "This invitation will expire on … (two weeks)"). parent_mailer is an initializer, so a server restart is needed to see it in dev. Verified: spec/mailers 77/0 (1 pre-existing pending). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert every ApplicationMailer template from the old repeated-Helvetica / #348eda-blue
tables into clean design-system fragments (slate body paragraphs + the brand cta_button
partial + brand links) that render inside the new mailer layout:
- casa_admin: account_setup, deactivation
- supervisor: account_setup, reimbursement_request, weekly_digest (+ partials — branded
links, a real divider, dropped the stray <button>)
- volunteer: account_setup, case_contacts_reminder, court_report_reminder, reimbursement_complete
- user: followup_notification, followup_resolved, password_changed_reminder
- fund_request: send_request; learning_hours: report_email (was a full standalone doc → fragment)
Copy preserved where spec-locked ("A {org}'s County Admin account", "Set Your Password",
"Fund Request", "This invitation will expire on …", the digest's 30-day line). Also fixed a
pre-existing N+1 in reimbursement_complete (reimbursement_amount was fetched twice) by
computing it once, so Prosopite passes.
Verified: spec/mailers 77/0 (1 pre-existing pending).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Other filters" selects passed `class:` in `f.select`'s options hash instead of html_options, so Rails dropped it and all three rendered with no class at all: unstyled native selects next to the styled ones, and missing `filter-input` -- the class `.filter-form` auto-submits on -- which left contact medium / want driving reimbursement / contact made completely inert. Pass html_options as the fourth argument. Also pass the current value to `options_from_collection_for_select`: with pre-rendered option HTML `f.select` cannot mark the selection, so the contact medium dropdown read "All" while its filter was active. Match the roster filter column (cases / volunteers / supervisors / reimbursements) instead of the taller form-field tokens: 38px controls (`py-2 pl-3` / `px-3 py-2`) and 12px slate-500 labels, measured identical to the volunteers and reimbursements bars. Rename the blank option "Display all" -> "All", the convention on every other filter; "Display all" was legacy Bootstrap copy carried through the migration untouched. Blank means "no filter" and each scope already no-ops on it. Document the filter-bar tokens, the All convention and the `f.select` trap in design.md, and guard all three in index_spec. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WAI-ARIA APG's disclosure rule is that the trigger's accessible name describes the content it controls, with `aria-expanded` carrying the state -- the same convention Material, USWDS, Polaris and Primer ship. "Expand / Hide" named the action twice, described nothing, and duplicated aria-expanded; the two case-contacts filter panels had even drifted to different casings of it. Filter panels: heading "Filter by" -> "Filters", trigger -> "More filters" (the panel holds the filters beyond the sticky Sorted by / Hide drafts / Reset filters, so a bare "Filters" would misdescribe it). Row expander aria-label "Toggle contact details" -> "Contact details". State affordance: only 2 of 8 triggers rotated their chevron. Every disclosure trigger now carries `group` + `group-aria-[expanded=true]:rotate-180` (`group-open:` for native details), and the reports "Filter columns" details and the all-casa change-password button get the chevron they lacked. Verified none -> 180deg on all five: both filter panels, users/edit change password + change email, all-casa change password, reports Filter columns. Tailwind v4 emits rotate-180 as the standalone `rotate` property, so the regression spec reads getComputedStyle().rotate -- `.transform` is "none" in both states and would pass a broken rotation. Left the case-contact card's inline "Show details"/"Hide details" details toggle alone: an inline item reveal, not a section header, and its text names the content. Documented all of it in design.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The mailer design-system restyle (5562398) rewrote 20+ templates but left the specs asserting the old copy. 18 assertions across 15 files, not the 4 the first partial run surfaced: - 14x "Click here to confirm your email" -> "Confirm my email", the CTA label the restyled confirmation_instructions renders. - email_changed dropped the possessive: "Your CASA account's email has been updated" -> "Your CASA account email has been updated". - 3x the invitation HTML lost "your new <Type> account" -> assert the copy it does render, "<Type> console account has been created for you", keeping the role-specific check. (invitation_instructions.text.erb was not restyled and still carries the old phrasing, which is why the text_part assertion in supervisors_spec passed throughout.) reset_password_link took links_in_email(email)[2]; the restyled reset email carries a single CTA link, so the magic index went nil. Find the link by URL instead so the layout's link count can change without breaking it. Full suite: 3554 examples, 19 failures, all pre-existing and none mailer- related -- 13 axe color-contrast on the sidebar's text-slate-400 group labels (2.63:1 vs AA 4.5:1), 2 view-spec SystemStackError from the settings rail calling edit_casa_org_path(current_organization), 4 known Selenium flakes that pass in isolation. Each verified against a stashed tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sidebar's uppercase group labels (Records / Activity / Reporting) and the
settings sub-nav rail's group labels were text-slate-400: 2.63:1 on white and
2.51:1 on the slate-50 page background, against the AA 4.5:1 threshold. Since
the shell is on every casa_app page this was 12 of the 13 axe failures.
design.md already banned it in three places ("never text-slate-400 for text --
fails AA", lines 33/117/222) while the app-shell and settings sections
documented the labels AS slate-400 -- the contradiction is what let it through,
so fix those two lines too.
slate-500 measures 4.77:1 on white and 4.55:1 on slate-50 (computed from the
oklch tokens; the script reproduces axe's 2.63/2.51 exactly), and stays quieter
than the rail's slate-600 items so the label/item hierarchy holds.
axe: 13 failures -> 1. The remainder is a pre-existing select-name violation on
the report filters' TomSelect multiselects, unrelated to contrast (that page
reported both violations before this change).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…laceholder
select-name (critical, the last axe failure). Both TomSelect controllers named
TomSelect's control input but not the native <select> underneath. TomSelect
repoints the <label for=...> at its own input, which empties select.labels, and
.ts-hidden-accessible *clips* the select rather than display:none-ing it -- so
it stays in the accessibility tree with no accessible name. Stamp the pre-init
name back on as an aria-label after init, in the controllers rather than
per-view, so every TomSelect is covered.
This is why design.md's claim that a `<label for>` picker "already resolves via
aria-labelledby" was wrong: the label resolves for TomSelect's input, never for
the select behind it. Corrected in both accessible-name sections. It also
explains why the violation count moved between runs (3 nodes, then 2): axe only
sees the selects that have finished initialising.
Verified in the DOM, not just via axe: all four report filters now carry their
label text ("Assigned to" / "Volunteers" / "Contact type" / "Contact type
group"), and the court-report picker gets "Case" -- a page axe doesn't test,
which had the same latent violation through searchable-select.
Contrast, the two remaining findings:
- A read notification's bell icon was slate-400 on a slate-100 tile, 2.40:1
against the 3:1 icon threshold; slate-500 is 4.35:1 and still recedes.
- design.md's Inputs token still documented placeholder:text-slate-400 (2.63:1).
All 50 placeholder sites in app/ already use slate-500, so the doc was the
only place left to reintroduce it from.
axe: 14/14 green. Full suite 3554 examples, 19 failures -> 5: the 2 pre-existing
view-spec SystemStackErrors and 3 Selenium flakes (each passes in isolation, a
different example each run, in a file that uses no TomSelect). Jest 126/126,
standardjs + erb_lint clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…bar row The collapsed card measured 960x144 for three controls, with a 45px "Filters" heading marooned 767px from its own "More filters" trigger, and "Reset filters" -- a 40px bordered :secondary, always rendered even at the defaults -- as the heaviest thing in it. Rebuilt as a single toolbar row, which is the list-toolbar standard (GitHub / Linear / Jira / Notion / Polaris): - Heading becomes sr-only: the controls speak for themselves next to a "More filters" trigger, the section stays named for AT, and it buys back a row. - Hide drafts moves inline beside the trigger. It is the one high-traffic boolean, so the standard is to surface it rather than bury it in the panel -- and in a row of labelled fields a bare checkbox had to fake a baseline with pb-2.5 (measured 1px off); beside the trigger it just centres. - Clear filters renders only when clicking it would change something, as a ghost rather than a bordered button. filters_applied? handles the cases that make this non-obvious: an unchecked checkbox still posts (no_drafts=0), array filters arrive as [""], and a default sort is not a user choice -- while a non-default sort is, since reset_filterrific resets sort too. Alignment needed care: the row is items-end, so a bare 28px action group bottom-aligns 5px below a 38px control's centre. min-h-[38px] + items-center on the group makes the centres coincide -- measured equal (cy 213) for the sort control, the checkbox and the trigger at 1280 and 700. 144px -> 100px desktop, 238px -> 142px mobile, same controls. The flagged new_design prototype gets the same Clear treatment so the two don't drift. case_contacts specs 135, helper specs 24/24 (unit-covering filters_applied? against real ActionController::Parameters -- a Hash stub would have hidden that Parameters is not Enumerable, which is how the first cut broke). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ter count Contact types was an exposed grid of ~25 checkboxes in ~10 groups: 502px on desktop and 954px on mobile -- taller than the results list it filters, so opening the panel pushed the data off screen (the whole card reached 1599px on a 390px phone). Past roughly ten options the standard is a searchable control (Jira / Linear / GitHub / Polaris), and this app already has one. As a single multiple-select TomSelect it is 42px; the expanded card drops to 415px desktop and 679px mobile. Groups become <optgroup>s and the chips are the "which types are on" readout, so nothing is lost. Two things had to be checked rather than assumed. TomSelect dispatches change with initEvent(name, true, false), so it bubbles and the delegated .filter-input auto-submit still fires -- a non-bubbling event would have made the filter inert exactly like the html_options bug. And the label has to name the native select, or the multiple-select controller finds no accessible name to copy and the select-name violation returns; measured aria-label="Contact types" after init. Active-filter count on the overflow trigger, as a brand-100/brand-700 pill rather than a parenthetical -- a digit inside the label reads as part of it, where a tinted pill signals "something is on" (Jira / Notion / Airtable). It counts fields not values (three types picked is 1) and excludes the filters already visible in the row, which would otherwise be double-reported. The flagged prototype gets the same multiselect, and with it a latent bug of its own: select_tag "casa_case_ids[]" is sanitised to id "casa_case_ids_", so its label's for="casa_case_ids" matched nothing and that control had no accessible name either. Explicit id: on both. case_contacts + helper + axe: 167 examples, 0 failures. axe still 14/14. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
remove_button only gives a per-chip x, so emptying a multiselect was a chip-at-a-time chore. Both controller paths now load clear_button, matching the searchable single-select, which already had it -- the two had silently diverged. The plugin's own CSS was the real problem: it ships the x at opacity 0 and reveals it only on :hover / .focus. So a control full of chips showed no way to empty it -- undiscoverable with a mouse, unreachable by hover on touch, and a tabindex=0 control sitting at zero opacity. Capybara caught this immediately (ElementNotFound on a visible-only match) where a DOM-presence check passed. Forced visible at .multi.has-items, with the chevron hidden unconditionally to match since they share the right edge. Single-select keeps the hover reveal -- there the x replaces the chevron rather than sitting beside it. Audited all 10 multiselects: every one now reports clear_button with role=button, tabindex=0, title="Clear all selections", slate-500 ink. Two findings fixed on the way: - The theme's clear-button ink was slate-400, 2.56:1, under the 3:1 icon floor -> slate-500 (4.76:1). Hit area forced to 1.5rem square; tom-select's x measures 23x22, just under the 24x24 target minimum. Added a focus-visible ring, since it is keyboard reachable. - Two competing placeholder rules in the theme; the `.ts-wrapper`-prefixed one outranks the bare one, so the slate-400 value was winning over the slate-500 below it. Consolidated to one slate-500 rule. - case_groups was the only multiselect with no placeholder value; added. Sort label "Sorted by" -> "Sort by" (Jira / Polaris). Deliberately not "Filter by": it changes the order, not which rows show. 36 examples on index_spec, 194 across every page with a multiselect, Jest 126/126, axe still 14/14. The one failure in the case_contacts run moves between examples across runs and passes in isolation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clicking clear on the contact types filter closed the whole panel. The clear
button was only how it surfaced -- the panel's open state was `expand_filters?`
("is a hidden filter active"), re-derived on every render, and the filter bar
full-page re-renders on every change. So the panel closed itself whenever a
change left no hidden filter on. Three reproductions, all measured
panel_hidden false -> true:
clear the last hidden filter -> closed
set a hidden select back to All -> closed
tick Hide drafts with panel open -> closed (no hidden filter touched)
The third is the tell: the user opened the panel, used an unrelated control, and
it slammed shut. The state was never the user's.
The disclosure controller now takes an optional `field` target and writes the
open state into it on toggle; the filter form carries a hidden filters_open, and
filters_open? honours it when present, falling back to expand_filters? only on
first load so a URL with filters still arrives open. Both directions verified --
a panel the user CLOSED stays closed even while a filter is active, which
deriving-from-params also got wrong.
Applied to every instance of the pattern, which turned up a second shape of the
same bug: users/edit change password + change email and all-casa-admins change
password hardcoded the panel shut, and update_password / update_email fail with
render :edit -- so the error rendered at the top of the page while the form it
referred to collapsed out of sight, taking the user's input with it. action_name
is still the failed action inside render :edit, so open exactly that panel and
leave its sibling shut. Not affected: the reports column-filter details (native
CSV submit, no re-render) and the new-design row expanders.
277 examples across case_contacts / users / all_casa_admins / reports / helper /
axe, 0 failures. The 4 new panel specs pass 4/4 repeat runs. Jest 126/126,
standardjs + standardrb + erb_lint clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… as headers Three problems in the contact-types multiselect. 1. A pick cost a full page load. The control wore `filter-input`, whose delegated change-submit re-renders the page, which tore down the open menu -- so picking N types meant N page loads and N reopenings. Confirmed by tagging `window` and watching the tag vanish on the first pick. closeAfterSelect is a red herring; it already defaults to false. Dropped `filter-input` and added submit-on-close: the controller holds the submit while the menu is open and fires once on dropdown_close, so three picks are now one render with the menu never closing. A change with the menu already shut (the clear x) still submits immediately, so clearing stays instant. Escape, clicking another control and blur all close-and-apply -- note a Capybara click on a non-focusable element does NOT close it, which made this look broken when it worked. 2. Group headers had no visual level. tom-select ships .optgroup-header at the options' own 13px/400 with 4px LESS left padding, so a dark line sat one notch out from the items and read as misaligned text. Now the sidebar's group-label token -- 12px/600 uppercase, 0.025em, slate-500 -- padded to the options' 0.75rem so the column edge is straight, with a slate-100 divider between groups. Measured: header 12px w600 uppercase, both at paddingLeft 12px. 3. The rich multiselect threw its grouping away. It carried `group` on every option but never passed `optgroups`, so TomSelect used it for search only and rendered 25 flat rows -- the same data as the filter, a different menu. casa_cases new/edit now shows 7 headers styled identically. On matching the add-new-case-contact page: its contact types stays an exposed grouped checkbox fieldset. It is that page's primary required input and each row carries a per-type recency hint that cannot survive collapsing into chips, so the filter's control would cost real information there. What is unified is the group label token, now identical across the fieldset, the menu headers and the sidebar. 306 examples across casa_cases / reports / case_groups / case_contacts / axe, 0 failures; the 7 contact-types specs pass 3/3 repeat runs. Jest 126/126, standardjs + standardrb + erb_lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rafts list Drafts accumulate because CaseContactsController#new inserts a row the moment the form opens, so every abandoned "New case contact" click leaves a `started` draft behind -- and nothing ever cleaned them up. 1. ExpireCaseContactDraftsService + a rake task for Heroku Scheduler, matching the existing scheduled-task pattern. Deletes `started` drafts untouched for CASE_CONTACT_DRAFT_EXPIRY_DAYS (default 7, WordPress's auto-draft window). `started` only: `details` means the user attempted a real submit and hit validation, `active` is a finished contact. `updated_at` is the liveness signal because autosave touches it, so a draft being written is never in range, and a non-positive window raises rather than deleting live work. HARD delete, not Paranoia's soft delete: CaseContact is acts_as_paranoid, and a soft-deleted draft keeps its row AND becomes MORE visible, since grab_all shows deleted records to CasaAdmins and the card prefixes "[DELETE]". Children go first -- additional_expenses and contact_topic_answers hold FK constraints so really_destroy! raises while they exist, and contact_topic_answers is itself paranoid so a plain destroy leaves its rows behind. 2. The org-wide drafts list was routed but linked from nowhere, so the mess was unmanageable. Added to a More overflow menu on the index, gated on the existing drafts? policy. 3. CaseContactReport now says `.active` explicitly. Drafts were excluded only by accident: they have a nil casa_case_id and the casa_org scope inner-joins casa_cases. Specs that pass no casa_org_id skip that join entirely, so the protection was one refactor away from exporting half-finished contacts into supervisors' CSVs. Three report-spec assertions compared relations with eq, which compares to_a and so asserted row order with no ORDER BY; adding a WHERE perturbed the plan. Now match_array, matching the sibling assertion in the same file. Also found and fixed while verifying: the test database held a leftover CasaOrg "CASA Org 1", and FactoryBot's name sequence restarts at 1 each process, so create(:casa_org) collided intermittently -- that, not flaky code, was the rotating failure in case_contact_report_spec (now 31/31 three runs running). db:test:prepare cleared it. 189 examples across services / report / requests / axe, 0 failures. Service spec covers the window, the status gate, the hard delete and the FK children. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…open CaseContactsController#new inserted a row so the wizard had an id to autosave into, so every abandoned "New case contact" click left a permanent empty draft. The record is now built unsaved and persisted at the first real save. new/create moved onto the wizard controller so they share prepare_form, finish_editing and case_contact_params (Wicked's setup_wizard no-ops when params[:id] is nil, so the extra actions sit there safely). GET /case_contacts/new renders; POST /case_contacts creates. A failed first submit now persists nothing at all, which is the point. Three places needed the id before it existed, not the one I expected. All now go through app/javascript/src/case_contact_draft.js, which owns the single creation path -- two would race and insert two drafts -- and hands the new id to everything keyed on it: - autosave: creates on the first save, then PATCHes the wizard step. Posting to create twice would have made a second draft on the second keystroke. - contact-topics: checking a topic POSTs a ContactTopicAnswer immediately, so it brings the contact into existence first. - casa-nested-form: "Add another expense" POSTs an AdditionalExpense against the parent id, and its own guard aborts on an empty one -- expenses were silently never saved until this was wired. Two bugs found by the specs on the way: the form needs data-case-contact-id so an EXISTING record isn't treated as unpersisted and re-created (this broke editing), and the create path has to dispatch autosave:success, which casa-nested-form listens for. Also added CaseContactPolicy#create? -- create fell through to ApplicationPolicy#create? (admin only), so volunteers got "not authorized". Specs updated where they asserted the old contract: page load no longer inserts, "Create another" reopens /case_contacts/new rather than a wizard path, and the carried-forward case is asserted in the markup rather than in a draft row. spec/system/case_contacts 133/133 green, including the examples that have been intermittently failing all session. requests + views + services + policies + axe 1669 examples with 2 failures, both the pre-existing view-spec SystemStackErrors. Jest 126/126, standardjs + standardrb + erb_lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two changes to the case-contacts filter card. A sort is not a filter. A non-default sort was enough to put a control labelled "Clear filters" on screen, and clicking it reset the ordering as well -- because it used reset_filterrific_url, which drops everything. Clear is now gated on an applied FILTER and goes to clear_filters_path, which keeps the sort. Verified: sort-only shows no Clear, and occurred_at_asc survives both Clear and a chip removal. Both that link and every chip's remove link always send a filterrific hash carrying sorted_by. Filterrific restores its session-persisted filters whenever the submitted hash is blank, so an empty one would hand back the filters the link exists to drop. Case scope and panel state ride along; neither is a filter. Applied-filter chips, one per active filter, each with its own x. A count says how many filters are on; chips say which -- and they sit outside the panel, so a collapsed panel no longer hides what is narrowing the list. That also answers the question that prompted this: gating Clear on the panel being open would have removed the escape hatch in exactly the state that needs it most (a filter applied while the panel is shut). Polaris / Jira / Linear / GitHub all keep the clear affordance independent of the popover. Values are written for a human: booleans read Yes/No, a medium is humanised, contact types are listed by name, and Hide drafts shows no value because the label already is the value. The x carries a title as well as an aria-label -- aria-label alone is invisible to Capybara's click_on, which matches text/title. index_spec 47/47 across three consecutive runs (5 new chip examples), helper 33/33 (8 new), axe still green. The one failure seen in the wider run passes in isolation and moves between examples -- the tracked Selenium flakiness. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bar behaved two ways at once. The legacy .filter-input inputs submit through a jQuery handler that bypasses Turbo, so they do a native full-page submit; the multiselect's deferred submit used requestSubmit(), which fires a real submit event that Turbo intercepts and -- because the form carries data-turbo-frame -- scopes to the RESULTS FRAME ONLY. Everything in the card lives outside that frame, so after a contact-type change the Clear action and the filter count silently kept their old values while every other control updated them. Measured: badge stuck at 1 with two filters applied. Deferred submit now uses native form.submit(), and the count goes 1 -> 2 as it should. My earlier evidence for that feature was weak and I should flag it: I asserted that tagging `window` and finding the tag intact proved no submit had fired during a picking session. A frame update preserves the document, so that proved only that no FULL navigation happened -- it could not distinguish "no submit" from "frame submit". The spec now asserts on the count badge, which has to change, and one example needed spare contact types: hideSelected empties the menu after the last pick and TomSelect closes it, which legitimately fires the deferred submit. Chips removed, per the simpler treatment. The panel auto-opens when a hidden filter is active, so the filters are already on screen in the normal case, and chips duplicated that while doubling the surface that has to stay in sync. Count badge plus Clear is the treatment. Kept from the previous commit: Clear is gated on an applied FILTER and preserves the sort, so a non-default sort alone still shows no Clear and clearing no longer reorders the list. Verified across every path: fresh (no Clear, no badge), Hide drafts (Clear, no badge), + medium (badge 1), + contact types via the multiselect (badge 2), + non-default sort (unchanged), Clear (everything gone, sort kept). index_spec 43/43 over three consecutive runs, helper 25/25, case_contacts + helper + axe 177 examples 0 failures. standardrb / standardjs / erb_lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The form was a dead end. Submit and "Submit & add another" were the only ways off
it, so a user who opened it to look -- or who is editing an existing contact and
changes their mind -- had nothing but the browser button. design.md already
requires this ("Back navigation on sub-pages") and already lists it as a
recurring gap; the case-contact form was simply never audited.
Uses the shared page_header back link, pointing at the EXISTING #leave action:
redirect_back_to_referer with the list as a fallback. That action was already
routed and nothing in the app linked to it -- another affordance lost in the
migration, like the drafts list and the app-shell footer. Verified from both entry
points: opened from a case page, Back returns to that case; opened from the list,
Back returns to the list. So Back lands exactly where a successful Submit would.
Labelled "Back", not "Back to X", because the destination is the referer and so
varies. Deliberately NOT "Cancel": the form autosaves, so on an existing contact
the changes are already persisted and offering to cancel them would be a lie --
which is also why the escape sits in the header as navigation rather than beside
Submit as a dismissive action.
new_spec + edit_spec 41 examples 0 failures (4 new), case_contacts + axe +
requests 170 with one rotating Selenium flake that passes in isolation.
standardrb + erb_lint clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Back leaves the form; this throws the draft away. Offered only when there is a draft to throw away -- persisted? && !active?. A brand-new form has nothing to discard (nothing is inserted until the first save, so Back is the whole exit), and an active contact is a real record, deleted from the list instead. Uses shared/_confirm_button, the documented destructive-confirm Dialog, with :danger_outline per the destructive-button rule. Deletion goes through a new CaseContact#discard!, which the expiry service now calls too, so one place knows how to take a draft apart: hard delete, children first, because additional_expenses and contact_topic_answers hold FK constraints and contact_topic_answers is itself paranoid. Hard rather than soft because a soft-deleted draft keeps its row and resurfaces to CasaAdmins as a "[DELETE]" row -- discarding would leave more clutter than it removed. Its own action rather than #destroy: that one redirects to request.referer, which here is the form of the record just deleted. #discard_draft returns where the form was opened from, so discarding lands in the same place as Back and Submit. Two traps, both found by driving the dialog rather than reading it: - shared/_confirm_button confirms via button_to, which renders its own <form>, and nested forms are invalid HTML. Inside the case-contact form the browser dropped the inner one and the confirm submitted the OUTER form, sending DELETE /case_contacts/:id/form/details -> routing error, while the dialog looked perfect. It now sits outside the form on its own bordered row, which is better placement for a destructive action regardless. - authorize @case_contact, :destroy? still resolved to discard_draft?; the policy now aliases it, like every other alias in CaseContactPolicy. case_contacts + services + model + policies + axe: 748 examples, 0 failures (5 new discard examples). standardrb + erb_lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g on time Two bugs on the new-case-contact form, both mine, both from driving the form instead of seeding state. 1. The relevant-cases dropdown showed a stray unclickable number above the cases. CasaCaseDecorator#hash_for_multi_select set `group: casa_org_id`. That was harmless while the multiselect only SEARCHED groups, but when I taught it to RENDER optgroups the raw org id became a header. Measured headers=["10100"]. Cases are already org-scoped so there is no group worth showing; removed. Contact types keep theirs -- theirs is a real name, verified still grouping as Legal / Family / Placement / Education / Health. 2. Discard draft never appeared. The control is server-rendered on `persisted?`, and the autosave that creates the draft never re-renders the page, so it only showed up after a manual reload. It now ships `hidden` and case_contact_draft.js reveals it and sets its action from a `discard_path` the create response hands back beside id and form_action -- no route rebuilt in JS. Measured: draft created by typing notes, button appears with no reload, its form action points at that record, confirming deletes it and returns to the case. I had the evidence for #2 and misread it: my earlier probe printed "after reload: discard present? true" and I treated that as a pass rather than noticing the reload was doing the work. Seeding a draft and visiting the wizard URL renders the persisted branch, so it never exercised the path a user takes. Two rack_test consequences of rendering the block up front, both in my own earlier specs: a `hidden` block is still "visible" to rack_test, so the absence check now asserts the class and the visible behaviour moved to a :js example; and the "no Cancel" claim is scoped to the actions row, since the confirm dialog has its own Cancel in the DOM. case_contacts + decorators + casa_cases + axe: 355 examples, 1 failure that passes 2/2 in isolation. new_spec 39/39 twice. standardrb / standardjs / erb_lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
You were right that it belonged with the other buttons. I had exiled it to its own row below the form and rationalised that as better placement for a destructive action, when the real reason was a constraint I had worked around instead of solving: shared/_confirm_button confirms via button_to, which renders its own <form>, and nested forms are invalid HTML -- inside the case-contact form the browser dropped the inner one and the confirm submitted the outer form. Solved rather than avoided. The partial takes a new `confirm_form:` local: the id of a bodyless `form_with url:, method:, id:` rendered outside the enclosing form. The confirm then renders as a plain submit owned by that form through HTML's `form` attribute, so the trigger and dialog can live wherever the design wants while the request still reaches the right action. Backwards compatible -- every other confirm_button caller keeps button_to, and banners / case groups / org settings / casa_cases specs all pass. Placement: same row as the submits but pushed to the far end (sm:ml-auto) rather than beside Submit, so a destructive action is grouped without sitting under a thumb aiming for the primary. That is the compose-toolbar shape -- Gmail puts Send at one end and discard at the other. Measured: discard is inside #contact-form-action-buttons, on the same line as Submit, right edge flush with the row's, no nested <form> under #case-contact-form, and confirming still deletes the draft and returns to the case it was opened from. Also reframed my own "no Cancel" assertion: the confirm dialog now lives in that row and legitimately has a Cancel, and rack_test cannot distinguish a closed dialog's contents from visible ones, so it asserts the actions the row offers instead of hunting for an absence. case_contacts + banners + case_groups + casa_org + casa_cases/edit + axe: 223 examples, 0 failures. standardrb / standardjs / erb_lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is a work in progress. Will flag when it is ready to merge.