diff --git a/src/components/campaign/municipality/MunicipalityListAdvisorsControl.tsx b/src/components/campaign/municipality/MunicipalityListAdvisorsControl.tsx index 21104f1c..5520e328 100644 --- a/src/components/campaign/municipality/MunicipalityListAdvisorsControl.tsx +++ b/src/components/campaign/municipality/MunicipalityListAdvisorsControl.tsx @@ -8,18 +8,18 @@ import type { MunicipalityRelationEntry } from '@/components/campaign/shared/Mun import { MunicipalityRelationEditor, type MunicipalityRelationMutationResult, + type MunicipalityRelationTriggerProps, } from '@/components/campaign/shared/MunicipalityRelationEditor' import { postCampaignJson } from '@/lib/campaignJsonRequest' import type { EligibleAdvisorOption, MunicipalityAdvisorSummary, } from '@/utilities/municipality/municipalityViewModels' -import type { ReactNode } from 'react' const ADVISORS_ENDPOINT = '/campanha/municipios/advisors' const SAVE_ERROR_MESSAGE = 'Não foi possível atualizar os assessores. Tente novamente.' -type MunicipalityListAdvisorsControlProps = { +type MunicipalityListAdvisorsControlProps = MunicipalityRelationTriggerProps & { municipalityID: number municipalityName: string currentAdvisorIDs: number[] @@ -27,10 +27,6 @@ type MunicipalityListAdvisorsControlProps = { advisorNamesById: ReadonlyMap options: EligibleAdvisorOption[] variant: CampaignCellEditOverlayVariant - /** B193 — dense mobile card trigger (labelled wrapping stack). */ - trigger?: (entries: MunicipalityRelationEntry[], emptyState: ReactNode) => ReactNode - /** B193 — dense card trigger styling override. */ - triggerClassName?: string } const entryOf = (option: { id: number; name: string }): MunicipalityRelationEntry => ({ diff --git a/src/components/campaign/municipality/MunicipalityListExpectedVotesControl.tsx b/src/components/campaign/municipality/MunicipalityListExpectedVotesControl.tsx index b9ffe674..fefc3265 100644 --- a/src/components/campaign/municipality/MunicipalityListExpectedVotesControl.tsx +++ b/src/components/campaign/municipality/MunicipalityListExpectedVotesControl.tsx @@ -17,6 +17,7 @@ import { Spinner } from '@/components/ui/Spinner' import { formatElectionNumber } from '@/lib/electionFormat' import { DEFAULT_VOTE_ESTIMATE_SCENARIO, + VOTE_ESTIMATE_SCENARIOS, voteEstimateScenarioLabels, voteEstimatesEqual, type VoteEstimateScenario, @@ -50,6 +51,8 @@ type MunicipalityListExpectedVotesControlProps = { value: VoteEstimateScenarioViewModel, activeScenario: VoteEstimateScenario, ) => ReactNode + /** B193 — dense card styling override (same slot as the sibling controls). */ + triggerClassName?: string } & ( | { variant: 'popover'; municipalityName?: string } | { variant: 'sheet'; municipalityName: string } @@ -62,6 +65,7 @@ export const MunicipalityListExpectedVotesControl = ({ pledgeCoverage, variant, trigger, + triggerClassName, }: MunicipalityListExpectedVotesControlProps) => { const scenarioContext = useMunicipalityEstimateScenarioOptional() const activeScenario = scenarioContext?.scenario ?? DEFAULT_VOTE_ESTIMATE_SCENARIO @@ -79,13 +83,19 @@ export const MunicipalityListExpectedVotesControl = ({ // An `aria-label` replaces the trigger's content, and the content here is the // number itself — so the label has to carry it, and name the scenario it - // belongs to (the picker can disagree with the column's sort, E9). - const activeValue = value[activeScenario] + // belongs to (the picker can disagree with the column's sort, E9). All three + // scenarios ride along: the strip shows the whole interval (B193), so the + // announcement must too. + const scenarioSummary = VOTE_ESTIMATE_SCENARIOS.map((scenario) => { + const scenarioValue = value[scenario] + return `${voteEstimateScenarioLabels[scenario]}: ${ + scenarioValue == null ? 'sem estimativa' : formatElectionNumber(scenarioValue) + }` + }).join('; ') const triggerLabel = [ 'Editar votos estimados', municipalityName ? ` em ${municipalityName}` : '', - ` — ${voteEstimateScenarioLabels[activeScenario]}: `, - activeValue == null ? 'sem estimativa' : formatElectionNumber(activeValue), + ` — ${scenarioSummary}`, ].join('') return ( @@ -98,7 +108,7 @@ export const MunicipalityListExpectedVotesControl = ({ triggerLabel={triggerLabel} triggerBusy={isPending} statusMessage={statusMessage} - triggerClassName="group flex w-full items-center justify-center" + triggerClassName={triggerClassName ?? 'group flex w-full items-center justify-center'} align="center" contentClassName="w-[15.5rem] p-3" preventPopoverAutoFocus diff --git a/src/components/campaign/municipality/MunicipalityListLeadershipsControl.tsx b/src/components/campaign/municipality/MunicipalityListLeadershipsControl.tsx index 1ae6ba2a..f8f41793 100644 --- a/src/components/campaign/municipality/MunicipalityListLeadershipsControl.tsx +++ b/src/components/campaign/municipality/MunicipalityListLeadershipsControl.tsx @@ -7,28 +7,24 @@ import type { MunicipalityRelationEntry } from '@/components/campaign/shared/Mun import { MunicipalityRelationEditor, type MunicipalityRelationMutationResult, + type MunicipalityRelationTriggerProps, } from '@/components/campaign/shared/MunicipalityRelationEditor' import { postCampaignJson } from '@/lib/campaignJsonRequest' import type { EligibleLeadershipOption, MunicipalityLeadershipSummary, } from '@/utilities/municipality/municipalityViewModels' -import type { ReactNode } from 'react' const LEADERSHIPS_ENDPOINT = '/campanha/municipios/leaderships' const SAVE_ERROR_MESSAGE = 'Não foi possível atualizar as lideranças. Tente novamente.' -type MunicipalityListLeadershipsControlProps = { +type MunicipalityListLeadershipsControlProps = MunicipalityRelationTriggerProps & { municipalityID: number municipalityName: string currentLeadershipIDs: number[] leadershipNamesById: ReadonlyMap options: EligibleLeadershipOption[] variant: CampaignCellEditOverlayVariant - /** B193 — dense mobile card trigger (labelled wrapping stack). */ - trigger?: (entries: MunicipalityRelationEntry[], emptyState: ReactNode) => ReactNode - /** B193 — dense card trigger styling override. */ - triggerClassName?: string } const entryOf = (option: { id: number; name: string }): MunicipalityRelationEntry => ({ diff --git a/src/components/campaign/municipality/MunicipalityListRowReadouts.tsx b/src/components/campaign/municipality/MunicipalityListRowReadouts.tsx index 76cfa2da..7d59b31d 100644 --- a/src/components/campaign/municipality/MunicipalityListRowReadouts.tsx +++ b/src/components/campaign/municipality/MunicipalityListRowReadouts.tsx @@ -90,17 +90,23 @@ export const TerritorialClassCardReadout = ({ municipality, }: { municipality: MunicipalityListViewModel -}) => ( - - - - - -) +}) => { + // `sem_base` has no reason to explain: the wrapper stays UNpositioned, so a + // tap on the dash falls through to the município link (tap-through + // contract) instead of intercepting it with a dead tooltip target. + const positioned = municipality.territorialClass !== 'sem_base' + return ( + + + + + + ) +} diff --git a/src/components/campaign/municipality/MunicipalityMobileCard.tsx b/src/components/campaign/municipality/MunicipalityMobileCard.tsx index 884d3a3c..4187afd8 100644 --- a/src/components/campaign/municipality/MunicipalityMobileCard.tsx +++ b/src/components/campaign/municipality/MunicipalityMobileCard.tsx @@ -1,6 +1,6 @@ 'use client' -import { ChevronDownIcon } from 'lucide-react' +import { ChevronDownIcon, CircleAlertIcon } from 'lucide-react' import Link from 'next/link' import { useState, type ReactNode } from 'react' @@ -87,7 +87,12 @@ const chipTriggerClassName = 'min-h-8 rounded-full px-2.5' /** Ghost trigger styling for the read-only relation groups (no hover pill). */ const relationTriggerClassName = 'min-h-0 rounded-md bg-transparent px-0 hover:bg-transparent' -const chipLabel = 'text-xs font-medium text-muted-foreground' +const ChipLabel = ({ children }: { children: ReactNode }) => ( + {children} +) + +/** Read-only city rows render a plain dash everywhere a control would sit. */ +const CityDash = () => — const RelationGroupTrigger = ({ label, @@ -99,11 +104,26 @@ const RelationGroupTrigger = ({ emptyState: ReactNode }) => (
- {label} + {label}
) +/** + * One third of the avatar row: the label + the relation control (or the + * read-only display) for a single association type. + */ +const RelationGroup = ({ label, children }: { label: string; children: ReactNode }) => ( +
+ {children ?? ( +
+ {label} + +
+ )} +
+) + /** * B193 — the dense mobile municipality card: header with the 2022 vote * position on the right, the estimate scenario bar with the active marker, @@ -226,7 +246,10 @@ export const MunicipalityMobileCard = ({
{isCity ? ( - Não registrada + + Tendência + Não registrada + ) : ( ( <> - Tendência + Tendência @@ -260,14 +283,14 @@ export const MunicipalityMobileCard = ({ triggerClassName={chipTriggerClassName} trigger={(level, note) => ( <> - Nível + Nível )} /> ) : ( - Nível + Nível
-
- {isCity ? ( - — - ) : isCoordinator ? ( + + {isCity ? null : isCoordinator ? ( ) : (
- Assessores + Assessores
)} -
-
- {isCity ? ( - — - ) : ( + + + {isCity ? null : ( )} -
-
- {isCity ? ( - — - ) : ( + + + {isCity ? null : ( )} -
+
{isCity ? ( - — + ) : update ? ( <>