diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md
index d1c4f106..fe861b7a 100644
--- a/docs/CHANGELOG-AGENTS.md
+++ b/docs/CHANGELOG-AGENTS.md
@@ -12,6 +12,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
**Recently resolved (2026-08-09):** **B180** (Issue #474) — as tools eleitorais legadas do Sollinha (`getTopDeputies`, `getMunicipalityVotes`) ganharam o **gate fail-closed de leader lockdown** que a B177 introduziu só na tool nova: `assertCanReadElectionData(ctx.user)` no topo do `execute` → `{ error: 'Leitura de dados eleitorais negada.' }` antes de qualquer query (o route `api/ai-chat` expõe todas as tools a todos os papéis). O padrão chegou a **3 call sites** → extraído um helper compartilhado `electionDataGate(ctx)` (`src/utilities/ai/tools/electionDataGate.ts`, `true | { error }`), e `getLeadingMunicipalities` refatorado para usá-lo (mensagem com fonte única). Unit tests novos (`electionToolsLockdown.unit.spec.ts`, 11): helper (leader nega/staff passa), as 3 tools com leader + payload stub intocado (fail-closed antes de query, inclusive no path SQL de terceiro candidato) e staff-proceeds por papel (coordinator/advisor/candidate). Sem migration/Consent/UI/prompt. Intenção [`sollinha-tools-eleitorais-leader-lockdown.md`](plans/sollinha-tools-eleitorais-leader-lockdown.md); impl [`sollinha-tools-eleitorais-leader-lockdown-impl.md`](plans/sollinha-tools-eleitorais-leader-lockdown-impl.md).
+**Recently resolved (2026-08-09):** **C97** (Issue #482) — Início/Término do overlay de criação rápida da agenda (C91) trocaram o `datetime-local` nativo (que renderiza AM/PM no idioma do browser e só deixava escolher o dia de forma confiável) por um **seletor shadcn de dia+horário 24h por construção**: `ActivityDateTimeField` (trigger com rótulo civil `dd/mm/aaaa às hh:mm` + popover com `Calendar` react-day-picker 9.7 `timeZone="America/Bahia"` locale pt-BR e selects nativos Hora 00–23 / Minuto em passos de 15). Contrato de valor inalterado (`YYYY-MM-DDTHH:mm` civil → `parseBahiaDateTimeInput` no submit), prefill do slot mantido, validações intactas; dia trocado no calendário preserva hora e vice-versa; popover com `max-h-[22rem]` + scroll (clippava no topo em viewports baixos — nav do calendário ficava inalcançável). Componente shadcn canônico `src/components/ui/Calendar.tsx` (variante radix-nova, lucide icons; `--cell-size` via `.cn-calendar` no styles.css) + helpers puros em `campaignTime.ts` (`formatBahiaCivilDateTimeLabel`, `hourOptions`/`minuteOptionsForStep`/`timeStepMinutes`, `floorToMinuteStep` — prefill de slot é múltiplo de 30, floor é defensivo). Única dep nova: `react-day-picker@^9.7.0` (date-fns transitivo). E2E do fluxo inline agora muda o horário pelo seletor e prova o chip "15:00 – 16:30" (24h) na grade. Sem migration/access/Consent. **Fatia mínima aprovada no gate**: eixo/chips do FullCalendar continuam `meridiem: 'short'` ("2 PM") e `formatBahiaDateTimeLabel` sem `hourCycle` explícito — follow-ups registrados no impl plan. Intenção [`c97-agenda-popover-seletor-data-hora.md`](plans/c97-agenda-popover-seletor-data-hora.md); impl [`c97-agenda-popover-seletor-data-hora-impl.md`](plans/c97-agenda-popover-seletor-data-hora-impl.md).
+
**Recently resolved (2026-08-09):** **B176** (Issue #458) — `/campanha/municipios` ganhou três dimensões de filtro na omnibox — **Dobradinha** (nome do contato), **Liderança** (nome do contato) e **Partido** (sigla) — com recortes "Sem dobradinha"/"Sem liderança"/"Sem partido" (precedente "Sem nível"). Params aditivos `stateDeputy`/`leadership`/`party` na URL congelada (sentinelas `sem_dobradinha`/`sem_lideranca`, reuso de `sem_partido`); `buildMunicipalityListWhere` ganhou um catálogo request-scoped fail-closed (`municipalityRelationSets.ts`, lazy: metade liderança só com filtro ativo) que vira as leituras reversa (`leadership.municipalities`) e cruzada (`stateDeputy.party`) em `where` — sem migration, paginação nativa mantida; facets estendidas (`stateDeputyIDs`/`leadershipOptions`/`parties`) e mapa compartilha o catálogo. **Coluna Dobradinhas liberada ao Assessor** (staff-wide): servidor já estava scoped (B37/B157 `reloadStaffActor` + `canUpdateMunicipality`), a mudança foi só de gate de UI (lista/mobile/picker) + catálogo full-staff para a célula (decisão do gate humano 2026-08-09). Extrato do par "sentinela + `exists:false`" em `campaignListUrl` (`NO_PARTY_FILTER_VALUE`, `splitAbsenceFilterValues`, `collapseListWhereOrBranches`) com 5 call sites. E2E: assessor vê/edita a coluna no portfólio; filtros pelas três dimensões; teste de coluna-restrita invertido. Intenção [`filtros-municipios-dobradinha-lideranca.md`](plans/filtros-municipios-dobradinha-lideranca.md); impl [`filtros-municipios-dobradinha-lideranca-impl.md`](plans/filtros-municipios-dobradinha-lideranca-impl.md).
**Recently resolved (2026-08-09):** **B174 reverso** (sem Issue — pedido direto) — a Sollinha (`/campanha/api/ai-chat`) voltou do `@ai-sdk/deepinfra` para o **`@ai-sdk/deepseek`** (API oficial `api.deepseek.com`): o cache-hit do V4-Flash direto custa **$0.0028/M** contra **$0.018/M** na Deep Infra (~6,4× mais barato), e a Sollinha tem prefixo estável (system prompt + tools + histórico) — o hit ratio alto derruba o custo efetivo mesmo com miss ($0.14 vs $0.09) e output ($0.28 vs $0.18) mais caros. Mesmo modelo (`deepseek-v4-flash`), mesmas tools; `@ai-sdk/deepseek` **pinado em `3.0.19`** (não `^3.0.19`, que flutuaria a 3.0.26 e puxaria a ferroviária `provider@4.0.7` duplicada — o lock ficou na `4.0.4/5.0.18` do `ai@7.0.47`, verificado no lockfile). **STT fica na Deep Infra** (B173): `whisper-large-v3` a $0.00045/min de áudio é o mais barato verificado (Groq $0.0019, OpenAI $0.006); `.env.example` agora documenta **as duas chaves** — `DEEPSEEK_API_KEY` (LLM) e `DEEPINFRA_API_KEY` (STT). Aviso da DeepSeek: aumento de preço anunciado ("em breve"). Sem migration, access, UI ou teste novo; smoke de deploy = 2–3 perguntas forçando tool calling real antes de confiar.
diff --git a/docs/plans/c97-agenda-popover-seletor-data-hora-impl.md b/docs/plans/c97-agenda-popover-seletor-data-hora-impl.md
new file mode 100644
index 00000000..28b60aa4
--- /dev/null
+++ b/docs/plans/c97-agenda-popover-seletor-data-hora-impl.md
@@ -0,0 +1,117 @@
+# Impl: Agenda — seletor de dia + horário (shadcn) no popover de criação rápida
+
+Status: aprovado
+Atualizado em: 2026-08-09
+Issue: #482
+Intenção: docs/plans/c97-agenda-popover-seletor-data-hora.md
+Appetite restante: herdado (~0,5–1 dia eng)
+
+## Leitura da intenção
+
+- **Outcome:** no overlay de criação rápida da agenda (popover desktop + bottom sheet mobile), Início e Término deixam de usar `datetime-local` e passam a usar um seletor shadcn de dia (calendário) **e** horário (passos selecionáveis); prefill do slot mantido e ajustável; salvar grava o horário efetivamente escolhido; fuso America/Bahia inalterado; fluxo pós-salvar inalterado. **Gate:** fatia mínima aprovada pelo humano — o AM/PM reportado é o `datetime-local` nativo do popover (renderiza no idioma do browser); o controle novo é 24h **por construção** (trigger `dd/mm/aaaa às hh:mm` + selects Hora 00–23 / Minuto 00–45, string civil pura).
+- **O que NÃO negociar:** prefill do slot clicado (semana/dia = slot 30 min; mês = 09:00–10:00); validações atuais (Início obrigatório; Término > Início); America/Bahia como fronteira de fuso (helpers de `campaignTime.ts`); overlay não vira o formulário completo; nada de all-day; passos de hora **visíveis e selecionáveis**.
+- **O que reavaliar:** a intenção aponta o seletor da doc shadcn (Calendar + horário). A composição de referência da shadcn usa `input type="time"` com o picker nativo escondido (digitado) — isso **não** satisfaz o aceite "passos visíveis e selecionáveis". A forma técnica é livre: manter o calendário da shadcn, trocar a parte de horário por selects de passos.
+
+## Abordagem recomendada
+
+```mermaid
+flowchart LR
+ A[ActivityInlineCreate] --> B[ActivityDateTimeField trigger + Popover]
+ B --> C[ui/calendar.tsx react-day-picker 9 timeZone America/Bahia]
+ B --> D[NativeSelect hora + minuto passo 15 min]
+ B --> E[civil string YYYY-MM-DDTHH:mm = contrato atual]
+ E --> F[submit/validação atuais inalterados]
+```
+
+**Opções consideradas:** A (recomendada) | B | C | D
+**Recomendação:** **A** — manter o contrato de valor civil `YYYY-MM-DDTHH:mm` (idêntico ao `datetime-local` atual), com um campo novo que renderiza trigger + popover shadcn (Calendar + 2 `NativeSelect` de hora/minuto em passos de 15 min). Zero mudança no caminho de submit/validação/parse (`parseBahiaDateTimeInput`), no prefill (`activitySlotPrefill`), em `activityUi.ts` ou no fluxo pós-salvar.
+
+### Decisões de engenharia
+
+**D1 — Dependência do calendário.**
+
+- Opções: A) `react-day-picker@^9.7.0` (versão pinada pela shadcn v4 — `apps/v4/package.json`) | B) `react-day-picker@10` (latest) | C) grade de mês própria.
+- Recomendação: A — o componente shadcn canônico `calendar.tsx` (variante radix-nova, compatível com o `components.json` do repo `style: radix-nova`) usa exatamente a API v9 (`DayPicker`, `getDefaultClassNames`, `DayButton`, `captionLayout`); `date-fns@4.1.0` vem como dependência transitiva, sem adicionar dep direta.
+- Rejeitadas: B porque a API v10 não é a do componente canônico e o `timeZone`/`DayButton` mudariam o código para uma variante não referenciada; C porque calendário acessível/i18n é rabbit hole (a11y de teclado, pt-BR, `today`, ranges) — reusar a peça shadcn é o padrão do repo.
+
+**D2 — Origem do `src/components/ui/calendar.tsx`.**
+
+- Opções: A) adaptar à mão o fonte canônico radix-nova (substituir `IconPlaceholder` por `lucide-react`, trocar imports `@/`) | B) `pnpm dlx shadcn add calendar`.
+- Recomendação: A — determinístico num worktree, sem risco de o CLI reescrever `components.json`/styles ou instalar versão divergente.
+- Rejeitada: B porque o CLI v4 pode resolver `react-day-picker@10` ou tocar arquivos fora do escopo; a adaptação manual do fonte pinado é 100% reproduzível.
+
+**D3 — Controle de horário (passos).**
+
+- Opções: A) dois `NativeSelect` (Hora 00–23 + Minuto 00/15/30/45, passo 15) | B) `input type="time"` com indicador nativo escondido (composição da doc shadcn) | C) steppers segmentados hora/minuto com botões +/− | D) um select com 48+ combinações.
+- Recomendação: A — passos visíveis e selecionáveis (aceite), nativo no mobile (roleta do SO), `NativeSelect` já existe em `src/components/ui/native-select.tsx` (mesma linguagem visual dos inputs do repo), testável em e2e sem depender de implementação de picker nativo do browser.
+- Rejeitadas: B porque é digitação, não seleção — não cumpre o aceite, e no mobile reabre o picker nativo pouco confiável que o item está eliminando; C porque é a superfície a11y mais cara (foco, aria, rolagem) sem ganho de produto; D porque 48 opções num select é pior de navegar que dois selects.
+- Passo: 15 min (recomendação de produto da intenção). Prefill de slot é sempre múltiplo de 15 (slots de 30 min; mês 09:00–10:00); se um valor fora do passo aparecer (defensivo), helper puro `floorToMinuteStep` arredonda para baixo + unit test.
+
+**D4 — Contrato de valor.**
+
+- Opções: A) manter civil string `YYYY-MM-DDTHH:mm` no estado do overlay | B) migrar o estado para instantes ISO `Date`.
+- Recomendação: A — o estado atual (`start`/`end` em `ActivityInlineCreate`) já é civil string; `parseBahiaDateTimeInput`/`formatIsoAsBahiaDateTimeInput` continuam sendo a fronteira única. O campo novo devolve/consome a mesma string, e o diff fica contido no componente.
+- Rejeitada: B porque reescreve o caminho de submit (validação, `createActivityInline`, `parseInlineResponsibles`) sem ganho de produto — custo de reverter alto, benefício zero.
+
+**D5 — Fuso no calendário.**
+
+- Opções: A) prop `timeZone="America/Bahia"` do RDP v9 + `locale={ptBR}` | B) construir/ler datas em UTC local do cliente.
+- Recomendação: A — o RDP 9.7 suporta `timeZone` nativamente; `selected` é ancorado num instante do meio-dia baiano (`parseBahiaDateTimeInput('T12:00')`) e `onSelect` deriva o dia civil com o helper existente `formatBahiaCivilDate` — robusto independentemente do fuso do browser (CI incluso).
+- Rejeitada: B porque quebraria nos browsers fora de UTC-3 (testes locais/CI) e duplicaria lógica que `campaignTime.ts` já tem.
+
+**D6 — 24h no popover (fatia mínima aprovada no gate).** O AM/PM que o usuário viu é o `datetime-local` nativo do popover: o browser renderiza o controle no idioma da UI (en-US → "2:30 PM") e não há como forçar 24h num controle nativo. O controle novo é 24h **por construção** — o trigger formata string civil pura (`dd/mm/aaaa às hh:mm`) e os selects são Hora 00–23 + Minuto em passos, sem meridiem — imune ao idioma do browser.
+
+- **Cortado no gate (follow-up registrado):** eixo de horário e chips do mês do FullCalendar (v7 usa `meridiem: 'short'` → "2 PM" mesmo com locale pt-br) e `hourCycle` explícito no `bahiaDateTimeDisplayFormatter` — o humano escolheu fatia mínima; virar follow-up se o produto quiser 24h na agenda e nos rótulos.
+
+### Componentes / mudanças
+
+- **`react-day-picker@^9.7.0`** (`package.json`): única dependência nova (date-fns transitivo).
+- **`src/components/ui/calendar.tsx`** (novo): componente shadcn radix-nova adaptado do fonte canônico (`apps/v4/registry/bases/radix/ui/calendar.tsx`); `IconPlaceholder` → `ChevronLeft`/`ChevronRight`/`ChevronDown` de `lucide-react`; imports `@/components/ui/button` + `@/lib/utils`.
+- **`src/app/(frontend)/styles.css`**: regras `.cn-calendar`, `.cn-calendar-dropdown-root`, `.cn-calendar-caption-label` (fonte `style-nova.css`) — definem `--cell-size:--spacing(7)` e `--cell-radius:var(--radius-md)` (token `--radius-md` já existe no arquivo, linha 83). O layout da campanha importa este CSS (layout de `(campaign)`).
+- **`src/lib/campaignTime.ts`** (owner de fuso/hora — editar, não twinar): helpers puros novos `formatBahiaCivilDateTimeLabel` (civil string → `dd/mm/aaaa às hh:mm`, 24h por construção), `timeStepMinutes = 15`, `hourOptions`/`minuteOptionsForStep`, `floorToMinuteStep`.
+- **`src/components/campaign/activity/ActivityDateTimeField.tsx`** (novo, convenção `Activity*` do domínio): `Field` + trigger `Button` (valor formatado + ícone de calendário, `id`/`aria-labelledby` para o `FieldLabel` existente) + `Popover` contendo `Calendar` e os dois `NativeSelect`; props `{ id, label, value: civil string, onValueChange, error, invalid }`; `onSelect` do calendário troca só a data mantendo hora/minuto atuais; selects trocam só hora/minuto mantendo a data.
+- **`src/components/campaign/activity/ActivityInlineCreate.tsx`**: substituir os dois `Input type="datetime-local"` por dois `ActivityDateTimeField` (Início \*, Término); estado `start`/`end` continua civil string; submit/validação/`parseBahiaDateTimeInput` **inalterados**. Como o novo controle não permite valor vazio, `end` deixa de poder ser limpo — o modelo continua aceitando `endAt` ausente, mas na prática o overlay sempre envia término (válido: o aceite exige Término > Início e nada de all-day). O AM/PM do `datetime-local` nativo some junto (24h por construção).
+- **Migration:** sem migration (sem schema).
+- **Access / Consent:** nenhum toque.
+- **UI:** Impeccable B — encaixe nos campos existentes: manter os dois campos na mesma grid `sm:grid-cols-2`, mesma altura visual dos inputs (`min-h-11`), erro no formato `FieldError` atual. Popover interno ancorado ao trigger (Radix vira quando falta espaço). Acessível: label visível + trigger `aria-labelledby`, calendário navegável por teclado (RDP), selects nativos.
+
+### Dados → forma
+
+N/A — controle de entrada, sem dados apresentados (intenção: "Vou apresentar dados? Não").
+
+## Fases verificáveis
+
+1. **Tracer — dependência + calendário** (quota pequena): `pnpm add react-day-picker@^9.7.0`; criar `ui/calendar.tsx`; regras CSS; smoke no dev (renderiza, `timeZone` ok, `ptBR` ok, células com tamanho certo). Verificar no browser com a agenda local.
+2. **Helpers puros**: `campaignTime.ts` + unit tests (`campaignTimeCivilDate.unit.spec.ts` ou spec irmã): `formatBahiaCivilDateTimeLabel`, `floorToMinuteStep`, opções de hora/minuto.
+3. **Campo novo + wiring**: `ActivityDateTimeField.tsx`; troca no `ActivityInlineCreate.tsx`; smoke do fluxo completo no dev (popover e drawer mobile): prefill do slot, trocar dia no calendário sem perder hora, trocar hora/minuto sem perder dia, salvar grava o escolhido, "Mais detalhes" intacto, trigger sempre 24h (sem AM/PM, qualquer idioma de browser).
+4. **Testes**: atualizar `tests/unit/activityInlineCreate.unit.spec.tsx` (trigger por label, valor via texto do botão; interação com calendário/selects — clicar dia, mudar hora, erro de Término ≤ Início); `activityInlineErrors.unit.spec.ts` se tocar os campos; novo spec unit do campo/pickers se precisar de interação isolada; e2e `tests/e2e/campaignActivity.e2e.spec.ts` — teste inline passa a ler o rótulo do trigger (sem `.inputValue()`), e ganha um passo que muda o horário pelo seletor e salva (aceite central: "Salvar grava o horário efetivamente escolhido").
+5. **Gates**: `pnpm gate:fast` → `pnpm test:int` → e2e afetado (`pnpm test:e2e` agenda) → `pnpm format:check`, `pnpm exec knip`, `pnpm check:cycles` → `pnpm build` (DB local) → scan Aikido dos arquivos tocados → registro no CHANGELOG-AGENTS → `pnpm push` → PR `--base main` `Closes #482` → auto-merge → `gh pr checks --watch --required`.
+
+## Rabbit holes / Não escopo (engenharia)
+
+- **Formulário completo e tarefas** (`ActivityForm.tsx`, `ActivityTaskFields.tsx`): continuam `datetime-local` — fora do escopo; o campo novo é independente o bastante para um swap futuro barato (registrar follow-up).
+- **Steppers segmentados / range no calendário / sincronizar Término com Início**: cortes de produto da intenção — não implementar.
+- **Persian/Hijri/RTL, `captionLayout="dropdown"`, presets, booked dates**: o componente canônico aceita via props, mas nada disso é usado.
+- **`generate:importmap`**: não se aplica (componentes de app, não de admin Payload).
+- **`datetime-local` em outros pontos do repo**: procurar e registrar follow-up único se houver mais ocorrências além do form/tarefas.
+
+## Riscos e mitigação
+
+- **Comportamento da prop `timeZone` do RDP 9.7.0**: mitigar com `selected` ancorado no meio-dia baiano + derivação via `formatBahiaCivilDate`; verificar no dev browser **antes** do e2e; se a prop não se comportar, fallback é formatar a seleção em America/Bahia via helpers existentes (mudança contida no campo).
+- **Popover aninhado (overlay da agenda + picker)**: Radix porta o conteúdo para `body`; padrão documentado da shadcn (date-picker em dialog); validar abertura/fechamento/clique fora no dev e no e2e.
+- **e2e existente usa `getByLabel('Início *').inputValue()`**: o trigger novo é um `Button` — `.inputValue()` falha; atualizar a asserção para o texto do rótulo (e a unidade correspondente). Não há outro teste que dependa do `datetime-local` do overlay (o form completo continua com input — testes dele intactos).
+- **CSS do calendário**: se as regras `.cn-calendar` não forem adicionadas, células perdem o tamanho padrão (`--cell-size`) — coberto na Fase 1 com smoke visual.
+- **Prefill fora do passo de 15 min**: hoje impossível (slots de 30 min / janela de 1h), mas `floorToMinuteStep` garante invariante e tem unit test.
+
+## Follow-ups registrados (fora da fatia mínima)
+
+- **Eixo de horário e chips do mês do FullCalendar em AM/PM** (v7: `DEFAULT_SLAT_LABEL_FORMAT`/`DEFAULT_TABLE_EVENT_TIME_FORMAT` com `meridiem: 'short'`): fix de 2 linhas (`slotLabelFormat`/`eventTimeFormat` com `meridiem: false`) — registrado porque o humano cortou no gate; produto decide.
+- **`hourCycle: 'h23'` explícito no `bahiaDateTimeDisplayFormatter`** (`formatBahiaDateTimeLabel`): hardening engine-proof (V8 já é h23; Safari/WebKit tem histórico de divergência) — mesmo item de produto acima.
+- **Trocar `datetime-local` do formulário completo e das tarefas** pelo `ActivityDateTimeField` — barato agora que o componente existe.
+
+## Aceite de engenharia
+
+- [ ] Aceite de produto da intenção ainda coberto (dia+horário selecionáveis, prefill mantido, validações, fuso, fluxo pós-salvar) + popover sem AM/PM (24h por construção no trigger e nos selects)
+- [ ] Invariantes AGENTS/engineering-standards (sem schema/access/Consent tocados; identificadores em inglês; copy pt-BR)
+- [ ] Testes de domínio previstos (unit helpers + unit campo + e2e agenda com mudança de horário)
+- [ ] `model-local: deepseek-v4-flash-high` registrado na Issue #482 (mapping canônico de `model: composer-2.5`; ausente no frontmatter)
diff --git a/package.json b/package.json
index 86e060ad..162f2b12 100644
--- a/package.json
+++ b/package.json
@@ -108,6 +108,7 @@
"payload": "3.82.0",
"radix-ui": "^1.4.3",
"react": "^19.2.4",
+ "react-day-picker": "^9.14.0",
"react-dom": "^19.2.4",
"react-hook-form": "^7.71.1",
"react-markdown": "^10.1.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 88b22131..74e4078a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -16,7 +16,7 @@ importers:
version: 4.0.50(react@19.2.4)(zod@4.3.6)
'@base-ui/react':
specifier: ^1.6.0
- version: 1.6.0(@date-fns/tz@1.2.0)(@types/react@19.2.14)(date-fns@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.14)(date-fns@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@fullcalendar/react':
specifier: 7.0.2
version: 7.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(temporal-polyfill@1.0.3)
@@ -101,6 +101,9 @@ importers:
react:
specifier: ^19.2.4
version: 19.2.4
+ react-day-picker:
+ specifier: ^9.14.0
+ version: 9.14.0(react@19.2.4)
react-dom:
specifier: ^19.2.4
version: 19.2.4(react@19.2.4)
@@ -573,6 +576,9 @@ packages:
'@date-fns/tz@1.2.0':
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
+ '@date-fns/tz@1.5.0':
+ resolution: {integrity: sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==}
+
'@dependents/detective-less@5.0.3':
resolution: {integrity: sha512-v6oD9Ukp+N7V4n6p5I/+mM5fIohSfkrDSGlFm5w/pYmchvbk+sMIHsLxrFJ5Lnujewj1BzWL0K84d88lwZAMQA==}
engines: {node: '>=18'}
@@ -3021,6 +3027,10 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
+ '@tabby_ai/hijri-converter@1.0.5':
+ resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==}
+ engines: {node: '>=16.0.0'}
+
'@tailwindcss/node@4.1.18':
resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
@@ -4242,6 +4252,9 @@ packages:
dataloader@2.2.3:
resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==}
+ date-fns-jalali@4.1.0-0:
+ resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
+
date-fns@3.6.0:
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
@@ -6555,6 +6568,12 @@ packages:
react: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc
+ react-day-picker@9.14.0:
+ resolution: {integrity: sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ react: '>=16.8.0'
+
react-dom@19.2.4:
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
peerDependencies:
@@ -8068,7 +8087,7 @@ snapshots:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
- '@base-ui/react@1.6.0(@date-fns/tz@1.2.0)(@types/react@19.2.14)(date-fns@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@base-ui/react@1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.14)(date-fns@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.7
'@base-ui/utils': 0.3.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -8078,7 +8097,7 @@ snapshots:
react-dom: 19.2.4(react@19.2.4)
use-sync-external-store: 1.6.0(react@19.2.4)
optionalDependencies:
- '@date-fns/tz': 1.2.0
+ '@date-fns/tz': 1.5.0
'@types/react': 19.2.14
date-fns: 4.1.0
@@ -8119,6 +8138,8 @@ snapshots:
'@date-fns/tz@1.2.0': {}
+ '@date-fns/tz@1.5.0': {}
+
'@dependents/detective-less@5.0.3':
dependencies:
gonzales-pe: 4.3.0
@@ -10552,6 +10573,8 @@ snapshots:
dependencies:
tslib: 2.8.1
+ '@tabby_ai/hijri-converter@1.0.5': {}
+
'@tailwindcss/node@4.1.18':
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -11725,6 +11748,8 @@ snapshots:
dataloader@2.2.3: {}
+ date-fns-jalali@4.1.0-0: {}
+
date-fns@3.6.0: {}
date-fns@4.1.0: {}
@@ -14546,6 +14571,14 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
+ react-day-picker@9.14.0(react@19.2.4):
+ dependencies:
+ '@date-fns/tz': 1.5.0
+ '@tabby_ai/hijri-converter': 1.0.5
+ date-fns: 4.1.0
+ date-fns-jalali: 4.1.0-0
+ react: 19.2.4
+
react-dom@19.2.4(react@19.2.4):
dependencies:
react: 19.2.4
diff --git a/src/app/(frontend)/styles.css b/src/app/(frontend)/styles.css
index 88cb0df5..16ea69f5 100644
--- a/src/app/(frontend)/styles.css
+++ b/src/app/(frontend)/styles.css
@@ -413,6 +413,20 @@ body,
}
}
+/* MARK: Calendar (shadcn radix-nova) — cell geometry tokens for the day grid.
+ `--cell-size`/`--cell-radius` are consumed by ui/calendar.tsx classNames. */
+.cn-calendar {
+ @apply p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(7)];
+}
+
+.cn-calendar-dropdown-root {
+ @apply border-input has-focus:ring-ring/50 border has-focus:border-ring has-focus:ring-3;
+}
+
+.cn-calendar-caption-label {
+ @apply h-6 pr-1 pl-1.5;
+}
+
/* E16 — municipality dossier print view: compact A4 typography (one step down
the DESIGN.md ramp: body text-sm→text-xs, section titles text-base→text-sm),
sections kept whole across page breaks. Chrome hiding lives in `print:`
@@ -423,7 +437,6 @@ body,
size: A4;
margin: 14mm;
}
-
html,
body,
#app {
diff --git a/src/components/campaign/activity/ActivityDateTimeField.tsx b/src/components/campaign/activity/ActivityDateTimeField.tsx
new file mode 100644
index 00000000..cec95963
--- /dev/null
+++ b/src/components/campaign/activity/ActivityDateTimeField.tsx
@@ -0,0 +1,131 @@
+'use client'
+
+import { CalendarIcon } from 'lucide-react'
+import { ptBR } from 'react-day-picker/locale'
+
+import { Button } from '@/components/ui/button'
+import { Calendar } from '@/components/ui/Calendar'
+import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
+import {
+ floorToMinuteStep,
+ formatBahiaCivilDate,
+ formatBahiaCivilDateTimeLabel,
+ hourOptions,
+ minuteOptionsForStep,
+ parseBahiaDateTimeInput,
+} from '@/lib/campaignTime'
+import { cn } from '@/lib/utils'
+
+/**
+ * C97 — shadcn day + time picker for the agenda quick create. The value
+ * contract is the civil `YYYY-MM-DDTHH:mm` string the old `datetime-local`
+ * used, so parse/validation on submit is untouched. 24h by construction:
+ * the trigger formats the civil string directly and the selects list
+ * 00–23 / 00–45, so no browser locale or Intl hour cycle can inject AM/PM.
+ * The parent renders the surrounding Field/FieldError; this component is the
+ * control itself (trigger + popover with calendar and step selects).
+ */
+export const ActivityDateTimeField = ({
+ id,
+ value,
+ onValueChange,
+ invalid = false,
+ errorId,
+}: {
+ id: string
+ value: string
+ onValueChange: (value: string) => void
+ invalid?: boolean
+ /** FieldError id to announce on focus — parity with the input it replaces. */
+ errorId?: string
+}) => {
+ const civil = floorToMinuteStep(value)
+ const datePart = civil.slice(0, 10)
+ const timePart = civil.slice(11)
+ const hour = timePart.slice(0, 2)
+ const minute = timePart.slice(3, 5)
+
+ const setDate = (date: Date | undefined) => {
+ if (!date) return
+ onValueChange(`${formatBahiaCivilDate(date)}T${timePart}`)
+ }
+
+ const setHour = (next: string) => onValueChange(`${datePart}T${next}:${minute}`)
+ const setMinute = (next: string) => onValueChange(`${datePart}T${hour}:${next}`)
+
+ const selectedInstant = parseBahiaDateTimeInput(`${datePart}T12:00`)
+
+ return (
+
+
+
+
+
+ {selectedInstant ? (
+
+ ) : null}
+