diff --git a/docs/plans/colunas-ao-lado-omnibox-listas-impl.md b/docs/plans/colunas-ao-lado-omnibox-listas-impl.md
new file mode 100644
index 00000000..b6d811cb
--- /dev/null
+++ b/docs/plans/colunas-ao-lado-omnibox-listas-impl.md
@@ -0,0 +1,59 @@
+# Impl: B137 — Colunas ao lado da omnibox (e fim do “Ordenado por …” residual)
+
+Status: aprovado
+Atualizado em: 2026-08-02
+Issue: #304
+Intenção: docs/plans/colunas-ao-lado-omnibox-listas.md
+Appetite restante: herdado (~0,5–1 dia eng)
+
+## Leitura da intenção
+
+- **Outcome:** Em todas as listas com seletor de colunas (B17), o botão **Colunas** fica na mesma fileira da omnibox (`trailing`); dados começam logo abaixo da barra. O span visível **“Ordenado por …”** some em Territórios, Lideranças e Dobradinhas (sort continua na omnibox/header + caption `sr-only`).
+- **O que NÃO negociar:** persistência/contrato do picker (cookie B17); `hidden md:flex` no mobile (gate 2026-08-02 opção A); lockdown de liderança; não mover sort para fora da omnibox/header.
+- **O que reavaliar:** hipótese de editar só `CampaignTable` — o picker precisa sair da tabela e entrar nos `*Filters` via `trailing`, porque a omnibox já vive nos filtros.
+
+## Abordagem recomendada
+
+**Opções consideradas:**
+
+- A) Manter picker em `CampaignTable` com CSS `absolute` ao lado da omnibox — rejeitada: DOM distinto, frágil entre listas com/sem cards mobile.
+- B) Slot `trailing` nos `*Filters` + remover picker de `CampaignTable` — **recomendada**: precedente B127 (`SaveMunicipalityFilterControl`), boundary de pending intacto.
+- C) Novo shell `CampaignListToolbar` — rejeitada: twin desnecessário.
+
+**Recomendação:** B — extrair `CampaignColumnPickerTrailing` (wrapper `hidden md:flex` + `CampaignColumnPicker`), helper `toCampaignColumnPickerColumns`, exportar metadados de colunas onde a tabela é dinâmica (municípios/apoiadores/territórios), páginas estáticas mapeiam `demandColumns`/`organizationColumns`/etc.
+
+### Componentes / mudanças
+
+- **`CampaignColumnPickerTrailing`** (`shared/CampaignColumnPickerTrailing.tsx`): client; encapsula breakpoint B17.
+- **`toCampaignColumnPickerColumns`** (`lib/campaignColumnVisibility.ts`): puro; mapeia `{id,label,mandatory}`.
+- **`CampaignTable`**: remove render do picker; mantém `columnVisibility` só para `resolveVisibleColumns`.
+- **`*Filters.tsx` (7 listas)**: prop opcional `trailing?: ReactNode` composta com trailing existente.
+- **Páginas/listas**: passam picker no `trailing` dos filtros; removem `
Ordenado por…
` visível.
+- **Exports:** `municipalityListPickerColumns`, `supporterPickerColumns`, `territoryListPickerColumns`.
+- **Migration:** sem migration.
+
+### Dados → forma
+
+N/A — só chrome de layout.
+
+## Fases verificáveis
+
+1. **Chassis** — helper + trailing component + remover picker de `CampaignTable`.
+2. **Wire-up** — 7 listas + organizações; remover sort span residual.
+3. **Gates** — `pnpm gate:fast`; push via `pnpm push`.
+
+## Rabbit holes / Não escopo (engenharia)
+
+- Picker mobile abaixo de `md` — fora (B17 lacuna).
+- Reordenar colunas / export / cenário na barra.
+
+## Riscos e mitigação
+
+- **Picker fora de `CampaignListResults`:** filtros já estão dentro de `CampaignListPendingBoundary` — `useCampaignListTransition` continua correto.
+- **Colunas dinâmicas (staff vs leader):** `municipalityListPickerColumns({ isStaffView })` espelha a mesma bifurcação da tabela.
+
+## Aceite de engenharia
+
+- [x] Aceite de produto da intenção ainda coberto
+- [x] Invariantes AGENTS/engineering-standards
+- [x] Testes de domínio previstos (unit existentes do picker; sem mudança de access/write)
diff --git a/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx b/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx
index a7e0abb6..e8d8ec62 100644
--- a/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx
@@ -8,11 +8,15 @@ import Link from 'next/link'
import { redirect } from 'next/navigation'
import { getPayload } from 'payload'
+import { CampaignColumnPickerTrailing } from '@/components/campaign/shared/CampaignColumnPickerTrailing'
import { CampaignListEmptyState } from '@/components/campaign/shared/CampaignListEmptyState'
import { CampaignListFooter } from '@/components/campaign/shared/CampaignListFooter'
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
import { SupporterFilters } from '@/components/campaign/supporter/SupporterFilters'
-import { SupporterList } from '@/components/campaign/supporter/SupporterList'
+import {
+ SupporterList,
+ supporterPickerColumns,
+} from '@/components/campaign/supporter/SupporterList'
import { SupporterListOverview } from '@/components/campaign/supporter/SupporterListOverview'
import { Button } from '@/components/ui/button'
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
@@ -102,6 +106,12 @@ export default async function SupportersPage({ searchParams }: SupportersPagePro
key={buildSupporterFiltersKey(state)}
state={state}
municipalityOptions={municipalityOptions}
+ trailing={
+
+ }
/>
{listBody}
diff --git a/src/app/(campaign)/campanha/(app)/demandas/page.tsx b/src/app/(campaign)/campanha/(app)/demandas/page.tsx
index a18c35a2..5f8fbc9d 100644
--- a/src/app/(campaign)/campanha/(app)/demandas/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/demandas/page.tsx
@@ -4,6 +4,7 @@ import Link from 'next/link'
import { getPayload } from 'payload'
import { DemandFilters } from '@/components/campaign/demand/DemandFilters'
+import { CampaignColumnPickerTrailing } from '@/components/campaign/shared/CampaignColumnPickerTrailing'
import { CampaignListEmptyState } from '@/components/campaign/shared/CampaignListEmptyState'
import { CampaignListFooter } from '@/components/campaign/shared/CampaignListFooter'
import {
@@ -14,6 +15,7 @@ import { CampaignTable, type CampaignTableColumn } from '@/components/campaign/s
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
import { Badge } from '@/components/ui/Badge'
import { Button } from '@/components/ui/button'
+import { toCampaignColumnPickerColumns } from '@/lib/campaignColumnVisibility'
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
import {
campaignDemandKindLabels,
@@ -117,7 +119,15 @@ export default async function DemandsPage({ searchParams }: DemandsPageProps) {
-
+
+ }
+ />
0
+ const isLeadershipVisible = !columnVisibility.hiddenColumnIds.includes('leaderships')
const [
{ rows, totalDocs, totalPages, filterFacets },
@@ -240,12 +240,15 @@ export default async function StateDeputiesPage({ searchParams }: StateDeputiesP
state={state}
partyOptions={partyFilterOptions}
hasNoParty={filterFacets.hasNoParty}
+ trailing={
+
+ }
/>
-
- {sortSummary}
-
{/* One shared Drawer for every chip-cell sheet on coarse pointers
(miss #52 — never a Drawer root per opened cell). */}
diff --git a/src/app/(campaign)/campanha/(app)/liderancas/page.tsx b/src/app/(campaign)/campanha/(app)/liderancas/page.tsx
index b6c37b32..507332ce 100644
--- a/src/app/(campaign)/campanha/(app)/liderancas/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/liderancas/page.tsx
@@ -11,6 +11,7 @@ import {
LeadershipFilterHead,
LeadershipSortableHead,
} from '@/components/campaign/leadership/LeadershipSortableHead'
+import { CampaignColumnPickerTrailing } from '@/components/campaign/shared/CampaignColumnPickerTrailing'
import { CampaignCopyableCell } from '@/components/campaign/shared/CampaignCopyableCell'
import { CampaignListFooter } from '@/components/campaign/shared/CampaignListFooter'
import {
@@ -40,7 +41,7 @@ import {
EmptyMedia,
EmptyTitle,
} from '@/components/ui/Empty'
-import { resolveVisibleColumns } from '@/lib/campaignColumnVisibility'
+import { toCampaignColumnPickerColumns } from '@/lib/campaignColumnVisibility'
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
import { formatBahiaDateTimeLabel } from '@/lib/campaignTime'
import {
@@ -327,8 +328,7 @@ export default async function LeadershipsPage({ searchParams }: LeadershipsPageP
])
const columnVisibility = await readCampaignColumnVisibility('liderancas')
- const isStateDeputyVisible =
- resolveVisibleColumns([{ id: 'stateDeputies' }], columnVisibility.hiddenColumnIds).length > 0
+ const isStateDeputyVisible = !columnVisibility.hiddenColumnIds.includes('stateDeputies')
const [
{ rows, totalDocs, totalPages, filterFacets },
@@ -415,12 +415,15 @@ export default async function LeadershipsPage({ searchParams }: LeadershipsPageP
municipalityFilterOptions={municipalityFilterOptions}
organizationFilterOptions={organizationFilterOptions}
stateDeputyFilterOptions={stateDeputyFilterOptions}
+ trailing={
+
+ }
/>
-
- {sortSummary}
-
{/* One shared Drawer for every chip-cell sheet on coarse pointers
(miss #52 — never a Drawer root per opened cell). */}
diff --git a/src/app/(campaign)/campanha/(app)/municipios/page.tsx b/src/app/(campaign)/campanha/(app)/municipios/page.tsx
index 3b3fd4f3..78312330 100644
--- a/src/app/(campaign)/campanha/(app)/municipios/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/municipios/page.tsx
@@ -9,8 +9,12 @@ import { getPayload } from 'payload'
import { RecentVisitTracker } from '@/components/campaign/dashboard/RecentVisitTracker'
import { MunicipalityEstimateScenarioProvider } from '@/components/campaign/municipality/MunicipalityEstimateScenarioContext'
import { MunicipalityFilters } from '@/components/campaign/municipality/MunicipalityFilters'
-import { MunicipalityList } from '@/components/campaign/municipality/MunicipalityList'
+import {
+ MunicipalityList,
+ municipalityListPickerColumns,
+} from '@/components/campaign/municipality/MunicipalityList'
import { MunicipalityListPageChrome } from '@/components/campaign/municipality/MunicipalityListPageChrome'
+import { CampaignColumnPickerTrailing } from '@/components/campaign/shared/CampaignColumnPickerTrailing'
import { CampaignListFooter } from '@/components/campaign/shared/CampaignListFooter'
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
@@ -101,6 +105,12 @@ export default async function MunicipalitiesPage({ searchParams }: Municipalitie
regionFilterOptions={columnFilterOptions.region}
advisorFilterOptions={columnFilterOptions.advisor}
slugFilterValues={columnFilterOptions.name}
+ trailing={
+
+ }
/>
)
diff --git a/src/app/(campaign)/campanha/(app)/organizacoes/page.tsx b/src/app/(campaign)/campanha/(app)/organizacoes/page.tsx
index 0894f502..d66671bf 100644
--- a/src/app/(campaign)/campanha/(app)/organizacoes/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/organizacoes/page.tsx
@@ -4,6 +4,7 @@ import Link from 'next/link'
import { getPayload } from 'payload'
import { OrganizationFilters } from '@/components/campaign/organization/OrganizationFilters'
+import { CampaignColumnPickerTrailing } from '@/components/campaign/shared/CampaignColumnPickerTrailing'
import { CampaignListEmptyState } from '@/components/campaign/shared/CampaignListEmptyState'
import { CampaignListFooter } from '@/components/campaign/shared/CampaignListFooter'
import {
@@ -14,6 +15,7 @@ import { CampaignTable, type CampaignTableColumn } from '@/components/campaign/s
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
import { Badge } from '@/components/ui/Badge'
import { Button } from '@/components/ui/button'
+import { toCampaignColumnPickerColumns } from '@/lib/campaignColumnVisibility'
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
import { organizationKindLabels } from '@/lib/schemas/organization'
import { readCampaignColumnVisibility } from '@/utilities/campaignColumnVisibilityCookie'
@@ -87,7 +89,15 @@ export default async function OrganizationsPage({ searchParams }: OrganizationsP
-
+
+ }
+ />
-
+
+ }
+ />
{
+export const DemandFilters = ({
+ state,
+ trailing,
+}: {
+ state: DemandListState
+ trailing?: ReactNode
+}) => {
const { navigate, isPending } = useCampaignListFilterNavigation({
state,
toHref: (next) => buildDemandListHref(next, 1),
@@ -68,6 +74,7 @@ export const DemandFilters = ({ state }: { state: DemandListState }) => {
onClearAll={() => {
runAction(clearDemandOmnibox(state))
}}
+ trailing={trailing}
/>
)
diff --git a/src/components/campaign/leadership/LeadershipFilters.tsx b/src/components/campaign/leadership/LeadershipFilters.tsx
index 981256c4..e55e6b8f 100644
--- a/src/components/campaign/leadership/LeadershipFilters.tsx
+++ b/src/components/campaign/leadership/LeadershipFilters.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useMemo, useState } from 'react'
+import { useMemo, useState, type ReactNode } from 'react'
import { CampaignListOmnibox } from '@/components/campaign/shared/CampaignListOmnibox'
import { useCampaignListFilterNavigation } from '@/components/campaign/shared/useCampaignListFilterNavigation'
@@ -24,11 +24,13 @@ export const LeadershipFilters = ({
municipalityFilterOptions,
organizationFilterOptions,
stateDeputyFilterOptions,
+ trailing,
}: {
state: LeadershipListState
municipalityFilterOptions: LeadershipFilterOption[]
organizationFilterOptions: LeadershipFilterOption[]
stateDeputyFilterOptions: LeadershipFilterOption[]
+ trailing?: ReactNode
}) => {
const { navigate, isPending } = useCampaignListFilterNavigation({
state,
@@ -132,6 +134,7 @@ export const LeadershipFilters = ({
onClearAll={() => {
runAction(clearLeadershipOmnibox(state))
}}
+ trailing={trailing}
/>
)
diff --git a/src/components/campaign/municipality/MunicipalityFilters.tsx b/src/components/campaign/municipality/MunicipalityFilters.tsx
index f4e6c888..8fb33f37 100644
--- a/src/components/campaign/municipality/MunicipalityFilters.tsx
+++ b/src/components/campaign/municipality/MunicipalityFilters.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useMemo, useState } from 'react'
+import { useMemo, useState, type ReactNode } from 'react'
import { useMunicipalityEstimateScenarioOptional } from '@/components/campaign/municipality/MunicipalityEstimateScenarioContext'
import { SaveMunicipalityFilterControl } from '@/components/campaign/municipality/SaveMunicipalityFilterControl'
@@ -32,6 +32,8 @@ type MunicipalityFiltersProps = {
advisorFilterOptions: MunicipalityFilterOption[]
/** Facet slugs (labeled from the catalog on the client). */
slugFilterValues?: readonly string[]
+ /** Beside the omnibox (B137): column picker, save bookmark, … */
+ trailing?: ReactNode
}
export const MunicipalityFilters = ({
@@ -40,6 +42,7 @@ export const MunicipalityFilters = ({
regionFilterOptions,
advisorFilterOptions,
slugFilterValues = [],
+ trailing,
}: MunicipalityFiltersProps) => {
const { navigate, isPending } = useCampaignListFilterNavigation({
state,
@@ -133,7 +136,12 @@ export const MunicipalityFilters = ({
onClearAll={() => {
runAction(clearMunicipalityOmnibox(state))
}}
- trailing={}
+ trailing={
+ <>
+ {trailing}
+
+ >
+ }
/>
)
diff --git a/src/components/campaign/municipality/MunicipalityList.tsx b/src/components/campaign/municipality/MunicipalityList.tsx
index 0f761667..15ea0250 100644
--- a/src/components/campaign/municipality/MunicipalityList.tsx
+++ b/src/components/campaign/municipality/MunicipalityList.tsx
@@ -35,6 +35,7 @@ import {
} from '@/components/ui/Empty'
import {
resolveVisibleColumns,
+ type CampaignColumnPickerColumn,
type CampaignColumnVisibility,
} from '@/lib/campaignColumnVisibility'
import { formatEngagementLevelLabel } from '@/lib/engagementLevel'
@@ -412,6 +413,29 @@ const municipalityListColumns = ({
] satisfies Array)),
]
+export const municipalityListPickerColumns = ({
+ isStaffView,
+}: Pick): CampaignColumnPickerColumn[] => {
+ const base: CampaignColumnPickerColumn[] = [
+ { id: 'name', label: municipalityColumnLabels.name, mandatory: true },
+ { id: 'region', label: municipalityColumnLabels.region },
+ { id: 'votos', label: municipalityColumnLabels.votos },
+ ]
+ if (!isStaffView) return base
+
+ return [
+ ...base,
+ { id: 'classe', label: municipalityColumnLabels.classe },
+ { id: 'level', label: municipalityColumnLabels.level },
+ { id: 'advisors', label: municipalityColumnLabels.advisors },
+ { id: 'trend', label: municipalityColumnLabels.trend },
+ { id: 'expectedVotes', label: municipalityColumnLabels.expectedVotes },
+ { id: 'lastSignal', label: municipalityColumnLabels.lastSignal },
+ { id: 'goalCoverage', label: municipalityColumnLabels.goalCoverage },
+ { id: 'lastUpdateAt', label: municipalityColumnLabels.lastUpdateAt },
+ ]
+}
+
export const MunicipalityList = (props: MunicipalityListProps) => {
const { municipalities, advisorNamesById, isStaffView, isCoordinator, advisorOptions, state } =
props
diff --git a/src/components/campaign/municipality/TerritoryFilters.tsx b/src/components/campaign/municipality/TerritoryFilters.tsx
index 85432793..4c1dcd1f 100644
--- a/src/components/campaign/municipality/TerritoryFilters.tsx
+++ b/src/components/campaign/municipality/TerritoryFilters.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useMemo, useState } from 'react'
+import { useMemo, useState, type ReactNode } from 'react'
import { CampaignListOmnibox } from '@/components/campaign/shared/CampaignListOmnibox'
import { useCampaignListFilterNavigation } from '@/components/campaign/shared/useCampaignListFilterNavigation'
@@ -22,9 +22,11 @@ import {
export const TerritoryFilters = ({
state,
regionOptions,
+ trailing,
}: {
state: TerritoryListState
regionOptions: TerritoryFilterOption[]
+ trailing?: ReactNode
}) => {
const { navigate, isPending } = useCampaignListFilterNavigation({
state,
@@ -81,6 +83,7 @@ export const TerritoryFilters = ({
onClearAll={() => {
runAction(clearTerritoryOmnibox(state))
}}
+ trailing={trailing}
/>
)
diff --git a/src/components/campaign/municipality/TerritoryList.tsx b/src/components/campaign/municipality/TerritoryList.tsx
index d76e6c37..541fb207 100644
--- a/src/components/campaign/municipality/TerritoryList.tsx
+++ b/src/components/campaign/municipality/TerritoryList.tsx
@@ -69,27 +69,20 @@ export const TerritoryList = ({
const sortSummary = formatTerritoryListSortSummary(sort, dir)
return (
- <>
-
- {sortSummary}
-
-
- row.variant === 'parent' ? row.region : `${row.parentRegion}-${row.label}`
- }
- rowId={(row) => (row.variant === 'parent' ? territoryAnchorId(row.region) : undefined)}
- rowClassName={(row) =>
- row.variant === 'sub' ? 'bg-muted/30' : 'scroll-mt-6 target:bg-muted/50'
- }
- empty={}
- />
- >
+ (row.variant === 'parent' ? row.region : `${row.parentRegion}-${row.label}`)}
+ rowId={(row) => (row.variant === 'parent' ? territoryAnchorId(row.region) : undefined)}
+ rowClassName={(row) =>
+ row.variant === 'sub' ? 'bg-muted/30' : 'scroll-mt-6 target:bg-muted/50'
+ }
+ empty={}
+ />
)
}
diff --git a/src/components/campaign/organization/OrganizationFilters.tsx b/src/components/campaign/organization/OrganizationFilters.tsx
index 91494bd9..73d3bd54 100644
--- a/src/components/campaign/organization/OrganizationFilters.tsx
+++ b/src/components/campaign/organization/OrganizationFilters.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useMemo, useState } from 'react'
+import { useMemo, useState, type ReactNode } from 'react'
import { CampaignListOmnibox } from '@/components/campaign/shared/CampaignListOmnibox'
import { useCampaignListFilterNavigation } from '@/components/campaign/shared/useCampaignListFilterNavigation'
@@ -18,7 +18,13 @@ import {
type OrganizationOmniboxAction,
} from '@/utilities/organization/organizationOmnibox'
-export const OrganizationFilters = ({ state }: { state: OrganizationListState }) => {
+export const OrganizationFilters = ({
+ state,
+ trailing,
+}: {
+ state: OrganizationListState
+ trailing?: ReactNode
+}) => {
const { navigate, isPending } = useCampaignListFilterNavigation({
state,
toHref: (next) => buildOrganizationListHref(next, 1),
@@ -76,6 +82,7 @@ export const OrganizationFilters = ({ state }: { state: OrganizationListState })
onClearAll={() => {
runAction(clearOrganizationOmnibox(state))
}}
+ trailing={trailing}
/>
)
diff --git a/src/components/campaign/shared/CampaignColumnPicker.tsx b/src/components/campaign/shared/CampaignColumnPicker.tsx
index e59b5b0a..2b160aba 100644
--- a/src/components/campaign/shared/CampaignColumnPicker.tsx
+++ b/src/components/campaign/shared/CampaignColumnPicker.tsx
@@ -15,16 +15,11 @@ import {
parseCampaignHiddenColumns,
serializeCampaignHiddenColumns,
toggleHiddenColumn,
+ type CampaignColumnPickerColumn,
type CampaignListId,
} from '@/lib/campaignColumnVisibility'
import { sameIdSet } from '@/lib/sameIdSet'
-type CampaignColumnPickerColumn = {
- id: string
- label: string
- mandatory?: boolean
-}
-
type CampaignColumnPickerProps = {
listId: CampaignListId
/** Every column of the table, hidden ones included — this is the menu. */
diff --git a/src/components/campaign/shared/CampaignColumnPickerTrailing.tsx b/src/components/campaign/shared/CampaignColumnPickerTrailing.tsx
new file mode 100644
index 00000000..44c6fda6
--- /dev/null
+++ b/src/components/campaign/shared/CampaignColumnPickerTrailing.tsx
@@ -0,0 +1,30 @@
+'use client'
+
+import { CampaignColumnPicker } from '@/components/campaign/shared/CampaignColumnPicker'
+import type {
+ CampaignColumnPickerColumn,
+ CampaignColumnVisibility,
+} from '@/lib/campaignColumnVisibility'
+
+type CampaignColumnPickerTrailingProps = {
+ columnVisibility: CampaignColumnVisibility
+ columns: readonly CampaignColumnPickerColumn[]
+}
+
+/**
+ * B137 — column picker beside the list omnibox (`trailing`), not stacked above
+ * the table. Gated at `md:` because municípios and apoiadores hide the table
+ * below that width and show cards instead (B17).
+ */
+export const CampaignColumnPickerTrailing = ({
+ columnVisibility,
+ columns,
+}: CampaignColumnPickerTrailingProps) => (
+
+
+
+)
diff --git a/src/components/campaign/shared/CampaignTable.tsx b/src/components/campaign/shared/CampaignTable.tsx
index d227b27f..560430de 100644
--- a/src/components/campaign/shared/CampaignTable.tsx
+++ b/src/components/campaign/shared/CampaignTable.tsx
@@ -1,7 +1,6 @@
import { Fragment, type ReactNode } from 'react'
import { CampaignCellTooltip } from '@/components/campaign/shared/CampaignCellTooltip'
-import { CampaignColumnPicker } from '@/components/campaign/shared/CampaignColumnPicker'
import { CampaignHoverTooltip } from '@/components/campaign/shared/CampaignHoverTooltip'
import {
Table,
@@ -140,9 +139,9 @@ export const CampaignTableHead = ({
type CampaignTableProps = {
columns: Array>
/**
- * B17 — passing this turns the column picker on. The caller reads the cookie
- * (`readCampaignColumnVisibility`) and hands the result over whole: hidden
- * columns are never rendered, so they cost nothing in the RSC payload either.
+ * B17 — hidden column ids from the cookie (`readCampaignColumnVisibility`).
+ * The picker itself renders in the omnibox `trailing` (B137); this prop only
+ * filters which columns the table paints.
*/
columnVisibility?: CampaignColumnVisibility
rows: readonly Row[]
@@ -178,74 +177,57 @@ export const CampaignTable = ({
const visibleColumns = resolveVisibleColumns(columns, columnVisibility?.hiddenColumnIds)
return (
- <>
- {columnVisibility ? (
- // Gated at `md:` because municípios and apoiadores hide this table
- // below that width (`className="hidden md:block"`) and show curated
- // cards instead — a picker over a table nobody can see is noise. The
- // cost is that the five surfaces which DO scroll this table on a phone
- // have no picker there; giving them one needs a per-caller seam, not a
- // breakpoint (registered as a fill-in in the B17 plan).
-
- ({ id, label, mandatory }))}
- hiddenColumnIds={columnVisibility.hiddenColumnIds}
- />
-
- ) : null}
-
-
- {caption ? {caption} : null}
-
-
- {visibleColumns.map((column) => (
-
- {column.head ?? {column.label}}
-
- ))}
-
-
-
- {rows.length === 0 && empty ? (
-
-
- {empty}
-
-
- ) : null}
- {rows.map((row) => (
-
- {visibleColumns.map((column) => {
- const cell = column.cell(row)
- const tooltip = column.cellTooltip?.(row)
-
- return (
-
- {tooltip ? (
- {cell}
- ) : (
- cell
- )}
-
- )
- })}
-
+
+
+ {caption ? {caption} : null}
+
+
+ {visibleColumns.map((column) => (
+
+ {column.head ?? {column.label}}
+
))}
-
-
-
- >
+
+
+
+ {rows.length === 0 && empty ? (
+
+
+ {empty}
+
+
+ ) : null}
+ {rows.map((row) => (
+
+ {visibleColumns.map((column) => {
+ const cell = column.cell(row)
+ const tooltip = column.cellTooltip?.(row)
+
+ return (
+
+ {tooltip ? (
+ {cell}
+ ) : (
+ cell
+ )}
+
+ )
+ })}
+
+ ))}
+
+
+
)
}
diff --git a/src/components/campaign/stateDeputy/StateDeputyFilters.tsx b/src/components/campaign/stateDeputy/StateDeputyFilters.tsx
index 8885352f..71dd7c87 100644
--- a/src/components/campaign/stateDeputy/StateDeputyFilters.tsx
+++ b/src/components/campaign/stateDeputy/StateDeputyFilters.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useMemo, useState } from 'react'
+import { useMemo, useState, type ReactNode } from 'react'
import { CampaignListOmnibox } from '@/components/campaign/shared/CampaignListOmnibox'
import { useCampaignListFilterNavigation } from '@/components/campaign/shared/useCampaignListFilterNavigation'
@@ -23,10 +23,12 @@ export const StateDeputyFilters = ({
state,
partyOptions,
hasNoParty,
+ trailing,
}: {
state: StateDeputyListState
partyOptions: StateDeputyFilterOption[]
hasNoParty: boolean
+ trailing?: ReactNode
}) => {
const { navigate, isPending } = useCampaignListFilterNavigation({
state,
@@ -83,6 +85,7 @@ export const StateDeputyFilters = ({
onClearAll={() => {
runAction(clearStateDeputyOmnibox(state))
}}
+ trailing={trailing}
/>
)
diff --git a/src/components/campaign/supporter/SupporterFilters.tsx b/src/components/campaign/supporter/SupporterFilters.tsx
index e02d7720..81202f8c 100644
--- a/src/components/campaign/supporter/SupporterFilters.tsx
+++ b/src/components/campaign/supporter/SupporterFilters.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useMemo, useState } from 'react'
+import { useMemo, useState, type ReactNode } from 'react'
import { CampaignListOmnibox } from '@/components/campaign/shared/CampaignListOmnibox'
import type { RelationOption } from '@/components/campaign/shared/RelationMultiSelect'
@@ -20,9 +20,11 @@ import { municipalityComboboxOptions } from '@/utilities/territory/territoryComb
export const SupporterFilters = ({
state,
municipalityOptions,
+ trailing,
}: {
state: SupporterListState
municipalityOptions: RelationOption[]
+ trailing?: ReactNode
}) => {
const { navigate, isPending } = useCampaignListFilterNavigation({
state,
@@ -109,6 +111,7 @@ export const SupporterFilters = ({
onClearAll={() => {
runAction(clearSupporterOmnibox(state))
}}
+ trailing={trailing}
/>
)
diff --git a/src/components/campaign/supporter/SupporterList.tsx b/src/components/campaign/supporter/SupporterList.tsx
index b6178e89..b289f253 100644
--- a/src/components/campaign/supporter/SupporterList.tsx
+++ b/src/components/campaign/supporter/SupporterList.tsx
@@ -5,7 +5,10 @@ import { CampaignTable, type CampaignTableColumn } from '@/components/campaign/s
import { Badge } from '@/components/ui/Badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
-import type { CampaignColumnVisibility } from '@/lib/campaignColumnVisibility'
+import {
+ toCampaignColumnPickerColumns,
+ type CampaignColumnVisibility,
+} from '@/lib/campaignColumnVisibility'
import { formatBrazilianPhoneInput } from '@/lib/phone'
import { supporterVoteIntentionLabels } from '@/utilities/supporter/supporterUi'
import type { SupporterListItemViewModel } from '@/utilities/supporter/supporterViewModels'
@@ -139,6 +142,8 @@ const supporterColumns: Array> =
},
]
+export const supporterPickerColumns = toCampaignColumnPickerColumns(supporterColumns)
+
export const SupporterList = ({ supporters, columnVisibility, empty }: SupporterListProps) => (
<>
diff --git a/src/lib/campaignColumnVisibility.ts b/src/lib/campaignColumnVisibility.ts
index 885dbef6..80b9ee53 100644
--- a/src/lib/campaignColumnVisibility.ts
+++ b/src/lib/campaignColumnVisibility.ts
@@ -42,6 +42,18 @@ export type CampaignColumnVisibility = {
hiddenColumnIds: readonly string[]
}
+/** Serializable column metadata for `CampaignColumnPicker` (B17 / B137). */
+export type CampaignColumnPickerColumn = {
+ id: string
+ label: string
+ mandatory?: boolean
+}
+
+export const toCampaignColumnPickerColumns = (
+ columns: readonly { id: string; label: string; mandatory?: boolean }[],
+): CampaignColumnPickerColumn[] =>
+ columns.map(({ id, label, mandatory }) => ({ id, label, mandatory }))
+
/**
* `listId:colA~colB|otherList:colC`. Every character here is legal in a
* cookie value (RFC 6265 forbids comma, semicolon, whitespace, quotes and