@@ -519,18 +519,53 @@ export function ListPage() {
519519 />
520520 < h1 className = "text-2xl font-semibold" > { listTitle } </ h1 >
521521 </ div >
522- { /* Header right-side: only the +Add primary action lives here.
523- Customize moved to the FilterBar trailing slot in #554 so the
524- filter row is one self-contained unit (… filter chips |
525- Clear all | Customize) — no second toolbar row, no dangling
526- chrome. */ }
527- < div className = "flex shrink-0 items-center gap-2" >
522+ { /* Header right-side toolbar (#608): one row holds every page-
523+ level affordance — Clear all (when filters are active),
524+ Refresh, Customize, then the primary `+ <Entity>` button.
525+ Previously these were split between the page header (Add)
526+ and the FilterBar trailing slot (Clear all / Refresh /
527+ Customize), which read as two toolbars; consolidating into
528+ one row matches the detail-page header layout (#572) and
529+ removes the second row of chrome.
530+ The `+ Add` label dropped the word "Add" — the leading `+`
531+ already signals "create", so "Add" was redundant (#608). */ }
532+ < div className = "flex shrink-0 flex-wrap items-center justify-end gap-2" >
533+ { activeFilterCount > 0 ? (
534+ < ResetButton
535+ isDirty
536+ onReset = { ( ) =>
537+ patchParams ( ( next ) => filters . forEach ( ( f ) => next . delete ( f . name ) ) )
538+ }
539+ label = "Clear all"
540+ icon = { < X className = "h-4 w-4" aria-hidden /> }
541+ title = "Clear all filters"
542+ />
543+ ) : null }
544+ < RefreshButton
545+ onRefresh = { refresh }
546+ tooltip = "Refresh"
547+ icon = { < RefreshCw className = "h-4 w-4" aria-hidden /> }
548+ />
549+ < button
550+ type = "button"
551+ onClick = { ( ) => setColsOpen ( true ) }
552+ aria-haspopup = "dialog"
553+ aria-label = "Customize columns"
554+ title = {
555+ hiddenCols . size > 0
556+ ? `Customize columns (${ hiddenCols . size } hidden)`
557+ : 'Customize columns'
558+ }
559+ className = "inline-flex shrink-0 items-center justify-center rounded-md border border-gray-300 px-2 py-1.5 text-sm hover:bg-gray-100"
560+ >
561+ < Settings2 className = "h-4 w-4" aria-hidden />
562+ </ button >
528563 { data . permissions . add && (
529564 < Link
530565 to = { withPreservedFilters ( `/${ appLabel } /${ modelName } /add` , searchParams . toString ( ) ) }
531566 className = "rounded-md border border-primary bg-primary px-3 py-2 text-sm font-medium text-white hover:opacity-90"
532567 >
533- + Add { data . verbose_name ? capitalize ( data . verbose_name ) : modelName }
568+ + { data . verbose_name ? capitalize ( data . verbose_name ) : modelName }
534569 </ Link >
535570 ) }
536571 </ div >
@@ -597,52 +632,9 @@ export function ListPage() {
597632 </ Popover >
598633 ) : null
599634 }
600- trailing = {
601- // Filter-row trailing slot (#554): "Clear all" + Refresh +
602- // Customize, in that order, as the last three buttons on
603- // the row. "Clear all" hides entirely when no filters apply
604- // (owner directive, v1.3.3) — the filter pills themselves
605- // signal there's nothing to clear, so a disabled button
606- // would be redundant chrome (CLAUDE.md §7). The Customize
607- // affordance is icon-only (the cog speaks for itself
608- // alongside Refresh — text + count chip were noise on a
609- // row that's already crowded).
610- < >
611- { activeFilterCount > 0 ? (
612- < ResetButton
613- isDirty
614- onReset = { ( ) =>
615- patchParams ( ( next ) => filters . forEach ( ( f ) => next . delete ( f . name ) ) )
616- }
617- label = "Clear all"
618- icon = { < X className = "h-4 w-4" aria-hidden /> }
619- title = "Clear all filters"
620- />
621- ) : null }
622- { /* Refresh (#592): refetch the changelist + filter counts
623- with the current filter / search / ordering / page
624- state preserved. Between Clear all and Customize. */ }
625- < RefreshButton
626- onRefresh = { refresh }
627- tooltip = "Refresh"
628- icon = { < RefreshCw className = "h-4 w-4" aria-hidden /> }
629- />
630- < button
631- type = "button"
632- onClick = { ( ) => setColsOpen ( true ) }
633- aria-haspopup = "dialog"
634- aria-label = "Customize columns"
635- title = {
636- hiddenCols . size > 0
637- ? `Customize columns (${ hiddenCols . size } hidden)`
638- : 'Customize columns'
639- }
640- className = "inline-flex shrink-0 items-center justify-center rounded-md border border-gray-300 px-2 py-1.5 text-sm hover:bg-gray-100"
641- >
642- < Settings2 className = "h-4 w-4" aria-hidden />
643- </ button >
644- </ >
645- }
635+ // FilterBar's `trailing` slot retired in v1.4.5 / #608 — Clear
636+ // all / Refresh / Customize live in the page header now so the
637+ // SPA shows one toolbar per page, not two.
646638 />
647639
648640 { editCount > 0 && (
0 commit comments