diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index 0d3cbc81..badd51db 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- +**Recently resolved (2026-08-09):** **B183** (Issue #501) — o auto-zoom do Safari iOS em inputs do PWA `/campanha` (font < 16px ao focar, escala que não volta no standalone) foi eliminado pela **causa**: uma regra CSS única em `styles.css` — `@media (pointer: coarse) and (hover: none)` + `[data-theme='campaign'] :is(input, select, textarea, [contenteditable='true']) { font-size: 1rem }` — cobrindo omnibox B127, Command search, native-select e textareas em qualquer orientação do iPhone (landscape chega a 932px de largura, o que descartou escopo por `max-width`). Unlayered vence as utilities `text-sm` sem `!important`; desktop (pointer fine) fica intocado; `Input`/`Textarea` do ui já eram `text-base md:text-sm`. Sem migration, access, Consent ou mudança de componente. E2E novo `tests/e2e/campaignIosInputZoom.e2e.spec.ts` pina o contrato: emulação `devices['iPhone 13']` → omnibox de `/campanha/municipios` computa **16px**; control desktop → permanece **14px**. Intenção [`pwa-ios-autozoom-input-foco.md`](plans/pwa-ios-autozoom-input-foco.md); impl [`pwa-ios-autozoom-input-foco-impl.md`](plans/pwa-ios-autozoom-input-foco-impl.md). + **Recently resolved (2026-08-09):** **B179** (Issue #462) — o Sollinha passou a falar de si **no masculino**: o system prompt (`src/utilities/ai/systemPrompt.ts`) abre com "Você é **o** Sollinha" e ganhou regra explícita (auto-referência masculina em primeira pessoa, nunca flexões femininas sobre si) — o welcome da UI já era masculino; o placeholder do chat virou "Pergunte para o Sollinha..." (`CampaignAIChat.tsx`) com os seletores e2e acompanhando (2 specs). Sem migration, access, Consent ou guarda automatizada (decisão do gate: só édito de texto). Os planos entregues da família (B162/B166/B167/B173/B174/B177) permanecem frozen com "a Sollinha" histórico — o que vale é a voz no runtime e o que se escreve daqui pra frente. Intenção [`sollinha-genero-masculino.md`](plans/sollinha-genero-masculino.md); impl [`sollinha-genero-masculino-impl.md`](plans/sollinha-genero-masculino-impl.md). **Recently resolved (2026-08-09):** **B181** (Issue #480) — a `/campanha/agenda` não exibe mais o bloco "Nenhum compromisso nesta janela e neste filtro" + botão "Criar atividade" acima do calendário em janela vazia (só ocupava espaço, redundante com slot-click C91, botão "Nova atividade" dos filtros e FAB C94). Remoção cirúrgica em `ActivityAgenda.tsx` (bloco condicional + órfãos `emptyHref`/imports `Link` e `buildActivityCreateHref`); `buildActivityCreateHref` permanece com 2 consumidores ("Mais detalhes" no inline create, "Nova atividade" nos filtros); loading/erro e `noEventsText` do FullCalendar (vista "Lista") intactos. Teste unitário de regressão: janela vazia não renderiza aviso nem link de criação. Sem migration, access, Consent ou schema. Intenção [`agenda-remover-aviso-vazio.md`](plans/agenda-remover-aviso-vazio.md); impl [`agenda-remover-aviso-vazio-impl.md`](plans/agenda-remover-aviso-vazio-impl.md). diff --git a/docs/plans/pwa-ios-autozoom-input-foco-impl.md b/docs/plans/pwa-ios-autozoom-input-foco-impl.md new file mode 100644 index 00000000..299d321d --- /dev/null +++ b/docs/plans/pwa-ios-autozoom-input-foco-impl.md @@ -0,0 +1,75 @@ +# Impl: PWA iOS: foco na omnibox de filtros aplica auto-zoom que não volta + +Status: aprovado +Atualizado em: 2026-08-09 +Issue: #501 +Intenção: docs/plans/pwa-ios-autozoom-input-foco.md +Appetite restante: herdado (~0,5–1 dia eng) — cabe com folga + +## Leitura da intenção + +- **Outcome:** nenhum input do PWA `/campanha` (omnibox, login, formulários, wizard) dispara o auto-zoom do Safari iOS ao receber foco; desktop e telas grandes mantêm a aparência atual. +- **O que NÃO negociar:** regra única (gate decidido: "todos de uma vez", inclui `contenteditable`/`select`); nada de `maximum-scale` global; anti-goal "subir para 16px no desktop". +- **O que reavaliar:** a hipótese de "fix campo a campo" — confirmado que é sistêmico: regra CSS única cobre. O ponto de escopo ("onde o trigger do iOS existe") pede decisão de engenharia (abaixo). + +## Abordagem recomendada + +```mermaid +flowchart LR + A[styles.css: media query touch] --> B[regra unlayered 16px em inputs do data-theme=campaign] + B --> C[omnibox / Command input / native-select / textarea / contenteditable] + D[e2e iPhone emulation: computed font-size 16px] --> E[desktop control: 14px intacto] +``` + +**Opções consideradas:** + +- **A — Regra CSS global no touch (recomendada):** bloco em `src/app/(frontend)/styles.css` (arquivo compartilhado frontend+campanha) dentro de `@media (pointer: coarse) and (hover: none)`, seletor `[data-theme='campaign'] :is(input, select, textarea, [contenteditable='true'])` com `font-size: 1rem`. Um bloco cobre omnibox (B127), Command search (ui `Command.tsx` `data-slot=command-input`), `native-select`, textareas e futuros contenteditable — nenhuma mudança de componente. +- **B — Bump por componente** (`text-base md:text-sm` no omnibox, CommandInput, native-select, …): o padrão que `Input`/`Textarea` já seguem, mas estendido caso a caso. +- **C — Regra global por largura** (`max-width: 767.98px`, espelhando o `md:` do repo). + +**Recomendação:** **A** — a intenção mandou regra única (gate 2026-08-09), e ela é a única que cobre o trigger em **todas** as orientações do iPhone: o auto-zoom do Safari existe também em landscape (larguras 844–932px em iPhones modernos), que a opção C perderia. `(pointer: coarse) and (hover: none)` é a expressão CSS canônica de "telefone touch" — o repo já usa variantes pointer (`pointer-fine:`/`pointer-coarse:` em `RelationChipCell`). Desktop/laptop (pointer fine) fica intocado — aceite 3 preservado. Como a regra é **unlayered**, ela vence as utilities `text-sm` em camada (cascata), mesmo com especificidade menor — sem `!important`. + +**Rejeitadas:** + +- **B** porque é whack-a-mole (rabbit hole nº 1 da intenção): hoje já há 3+ bespoke offenders e o trigger é regra do navegador, não do componente; um `` novo em `text-sm` reintroduz o bug silenciosamente. +- **C** porque perde o iPhone landscape (bug permanece vivo no aparelho do usuário) e cria dois regimes de fonte (16px portrait / 14px landscape) sem ganho. +- **`maximum-scale=1`/viewport fixo** — rejeitado pela própria intenção (degrada acessibilidade; vive no B182 como fallback se for preciso). + +### Componentes / mudanças + +- **`styles.css`** (`src/app/(frontend)/styles.css`, junto ao bloco `[data-theme='campaign'] :is([data-slot=…])` de bordas): um bloco `@media (pointer: coarse) and (hover: none)` com `[data-theme='campaign'] :is(input, select, textarea, [contenteditable='true']) { font-size: 1rem; }` + comentário curto explicando o limiar de 16px do Safari iOS (precedente de comentários explicativos no arquivo). +- **`Input`/`Textarea`/`InputGroup`** (`src/components/ui/`): **intocados** — já são `text-base md:text-sm`; a regra é no-op para eles no touch (16px) e não vaza para desktop. +- **Migration:** nenhuma. **Access / Consent:** nenhum. +- **UI:** Impeccable A (bug fix, sem redesenho). +- **Verificação:** e2e novo com emulação de device (abaixo). + +## Fases verificáveis + +1. **Regra CSS** — editar `styles.css`; conferir com o dev server que a omnibox em emulação touch computa 16px e desktop 14px. +2. **E2E** — `tests/e2e/campaignIosInputZoom.e2e.spec.ts`: + - describe com `test.use({ ...devices['iPhone 13'] })` (390×664, `isMobile`+`hasTouch` → `pointer: coarse`, `hover: none`): fixture `campaign` cria coordinator → login → `/campanha/municipios` → `getByRole('combobox', { name: 'Filtrar municípios' })` → `expect.poll(computed font-size) === '16px'`. Falha hoje (14px), passa com a regra. + - describe desktop (projeto campaign padrão): mesma omnibox permanece **14px** — pina o aceite "desktop continua como hoje". +3. **Gates** — `pnpm gate:fast`; `pnpm test:e2e` (spec novo + vizinhos de lista para regressão); `pnpm format:check`, `pnpm exec knip`, `pnpm check:cycles`, `pnpm build`. Entrega com `pnpm push`. +4. **CHANGELOG** — entrada curta em `docs/CHANGELOG-AGENTS.md`. + +## Rabbit holes / Não escopo (engenharia) + +- Inventário exaustivo de inputs (intenção já cortou; a regra cobre todos por definição). +- `maximum-scale` / política de escala global (vive no B182). +- Público site (`(frontend)`): fora do escopo, o seletor exige `data-theme='campaign'`. +- Bump de fonte em **buttons** (Select/Combobox triggers são botões — iOS não zooma botões). +- `RelationChipCell` inline input: é `pointer-fine:block` (oculto no touch) — a regra não o afeta, e não é preciso. + +## Riscos e mitigação + +- **iPad (touch, ≥768px) passa a 16px** (hoje `md:text-sm` = 14px): o iPadOS não tem o trigger de zoom; é mudança cosmética no tamanho padrão de formulário do iOS. Não afeta desktop. Mitigação documentada no impl plan — aceite "telas maiores" refere-se a desktop, que fica intacto. +- **Inputs com fonte > 16px em touch seriam reduzidos para 1rem:** nenhum no codebase hoje (varredura por `text-lg|xl|2xl` em inputs não achou nada); se um dia houver, a regra é o limite inferior correto (16px é o teto do trigger). +- **E2E mobile depende de emulação Chromium** (`devices['iPhone 13']`), não de Safari real: valida o contrato CSS (pointer/hover) que o Safari iOS avalia; a regra é a padrão da indústria (West-Wind/t3code/BookStack). +- **Layout reflow no touch:** 16px em inputs raros (ex. chips) — os chips da omnibox não são inputs; o campo em si cresce ~2px de altura (min-h fixos absorvem). + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto (regra única; desktop intacto; sem maximum-scale) +- [x] Invariantes AGENTS/engineering-standards (sem migration/access/Consent; copy pt-BR intacta; identificadores em inglês) +- [x] Testes previstos: e2e touch (16px) + e2e desktop (14px) — comportamento pina o aceite +- [x] Depth check: zero módulos novos; um bloco CSS no arquivo dono do theming diff --git a/src/app/(frontend)/styles.css b/src/app/(frontend)/styles.css index 16ea69f5..432c6e1a 100644 --- a/src/app/(frontend)/styles.css +++ b/src/app/(frontend)/styles.css @@ -269,6 +269,18 @@ body, border-radius: var(--radius-md); } +/* B183 — Safari iOS auto-zooms any form control whose computed font-size is + below 16px when focused, and a standalone PWA does not always restore the + scale when the keyboard closes. One unlayered rule for touch phones (coarse + pointer, no hover) keeps every campaign input — omnibox, Command search, + native selects, textareas — at the 16px threshold in any orientation, while + desktop keeps its own size utilities (unlayered beats the utilities layer). */ +@media (pointer: coarse) and (hover: none) { + [data-theme='campaign'] :is(input, select, textarea, [contenteditable='true']) { + font-size: 1rem; + } +} + /* The public-site base `h1` is centered + display-sized for article heroes. Campaign dashboard headings are left-aligned section titles, so undo the base centering and text balancing here; each heading keeps its own diff --git a/tests/e2e/campaignIosInputZoom.e2e.spec.ts b/tests/e2e/campaignIosInputZoom.e2e.spec.ts new file mode 100644 index 00000000..8583ebfb --- /dev/null +++ b/tests/e2e/campaignIosInputZoom.e2e.spec.ts @@ -0,0 +1,55 @@ +import { devices, type Page } from '@playwright/test' + +import { expect, test } from './fixtures/campaignE2EFixtures.js' + +const municipalityOmnibox = (page: Page) => + page.getByRole('combobox', { name: 'Filtrar municípios' }) + +const omniboxFontSize = (page: Page) => + municipalityOmnibox(page).evaluate((element) => getComputedStyle(element).fontSize) + +/** + * B183 — Safari iOS auto-zooms any focused form control whose computed + * font-size is below 16px, and a standalone PWA does not always restore the + * scale when the keyboard closes. The fix is one CSS rule for touch phones + * (`(pointer: coarse) and (hover: none)`), so these specs pin the computed + * size contract: 16px on a touch device, unchanged 14px on desktop. + */ +test.describe('B183 — inputs no touch ficam no limiar de 16px do auto-zoom iOS', () => { + // The project pins Desktop Chromium, so spread only the mobile-emulation + // bits of the iPhone descriptor (its `defaultBrowserType: 'webkit'` would + // force a new worker inside a describe group). + test.use({ + userAgent: devices['iPhone 13'].userAgent, + viewport: devices['iPhone 13'].viewport, + deviceScaleFactor: devices['iPhone 13'].deviceScaleFactor, + isMobile: devices['iPhone 13'].isMobile, + hasTouch: devices['iPhone 13'].hasTouch, + }) + + test('omnibox de municípios computa 16px em dispositivo touch', async ({ campaign, page }) => { + const coordinator = await campaign.fixtures.createCampaignUser('coordinator', { + name: campaign.fixtures.value('Coordenadora touch'), + }) + await campaign.login(page, coordinator.email!, coordinator.password) + await page.goto(`${campaign.baseURL}/campanha/municipios`) + + const omnibox = municipalityOmnibox(page) + await expect(omnibox).toBeVisible() + await expect.poll(() => omniboxFontSize(page)).toBe('16px') + }) +}) + +test.describe('B183 — desktop preserva o tamanho atual', () => { + test('omnibox de municípios continua em 14px', async ({ campaign, page }) => { + const coordinator = await campaign.fixtures.createCampaignUser('coordinator', { + name: campaign.fixtures.value('Coordenadora desktop'), + }) + await campaign.login(page, coordinator.email!, coordinator.password) + await page.goto(`${campaign.baseURL}/campanha/municipios`) + + const omnibox = municipalityOmnibox(page) + await expect(omnibox).toBeVisible() + await expect.poll(() => omniboxFontSize(page)).toBe('14px') + }) +})