Skip to content

feat(web): the screen says what is happening, instead of looking stuck (E13-T06, T07, T08) - #107

Merged
danielgorgonha merged 1 commit into
mainfrom
feat/tell-the-person-what-is-happening
Sep 21, 2026
Merged

danielgorgonha merged 1 commit into
mainfrom
feat/tell-the-person-what-is-happening

Conversation

@danielgorgonha

Copy link
Copy Markdown
Member

Quinta fatia do E13 (#23). Três respostas para a mesma pergunta: "travou ou está carregando?"

A linha de progresso no topo

Ela precisava de um ponto único por onde toda busca passa, e o lib/api.ts tinha dezenove fetch soltos. O lib/progress.ts é esse ponto, e um teste afirma que nenhum escapou: quem acrescentar a vigésima chamada ganha a barra sem precisar lembrar dela, que é a única forma de um indicador global continuar verdadeiro com o tempo.

Três decisões que não são óbvias:

  • Ela conta em vez de ligar e desligar. Duas buscas simultâneas terminando fora de ordem apagariam a barra com trabalho ainda rodando.
  • Para de crescer em 80%, em vez de fingir que sabe o tempo que ninguém mede. É o que faz barra chegar a 99% e ficar lá.
  • É aria-hidden de propósito. Cada tela já tem o seu role="status" dizendo que está carregando, e anunciar as duas coisas faria a pessoa ouvir o mesmo aviso duas vezes por busca.

Para quem pede menos movimento, a transição some e a barra salta de largura. Continua dizendo a mesma coisa, porque o sinal está na largura, não no deslizamento.

Sete telas trocaram "Carregando" pelo contorno

A palavra sozinha não diz quanto nem o quê. O contorno diz: são três parágrafos, é um cartão, é uma lista. Para quem lê com esforço, isso muda a decisão de esperar ou sair.

A palavra continua lá, escondida. Quem navega ouvindo precisa da palavra, não do contorno — trocar um pelo outro consertaria a tela de quem enxerga e pioraria a de quem não. E com erro o que aparece é a frase, não o desenho do que não vem mais.

Sem animação de brilho: é enfeite, custa bateria em celular básico e some de todo jeito para quem pede menos movimento.

A faixa de sem conexão

Sem ela, a pessoa toca, nada acontece, e a tela culpa o nosso lado — que é mentira. O texto separa o que continua funcionando do que espera: ler a explicação que já está na tela funciona; enviar respostas, não. Essa diferença decide se ela guarda o celular ou continua lendo.

Dois erros meus, e só um foi acusado

O OfflineBar começou com useState dentro de efeito, e o lint recusou com razão. Passou a usar useSyncExternalStore, que é a ferramenta para assinar estado que vive fora do React — e que também responde pelo servidor, onde navigator não existe.

O segundo ninguém acusou, e é o que me preocupa mais. Eu tinha posto largura na lista de dependências do efeito do RouteProgress para satisfazer uma guarda, e com isso o efeito reassinava a cada crescimento da barra, deixando o tempo pendente da rodada anterior sem quem o cancelasse. Nem tsc, nem lint, nem teste pegam isso. Achei relendo.

A regra que fica: dependência nova num efeito que assina algo é sinal de que a guarda está no lugar errado. A guarda virou referência, as dependências ficaram vazias, e o tempo é cancelado na limpeza.

89 testes, lint limpo, build limpo.

E13-T06, T07 and T08. Three answers to the same question: "is it stuck or is it loading?".

The progress line at the top needed a single point every fetch goes through, and lib/api.ts had nineteen
loose ones. lib/progress.ts is that point, and a test asserts none escaped: whoever adds the twentieth call
gets the bar without having to remember it, which is the only way a global indicator stays true over time.
It counts rather than flags, because two overlapping fetches finishing out of order would erase a bar with
work still running, and it stops growing at 80% instead of pretending to know the part nobody can measure.

The bar is aria-hidden on purpose. Every screen already carries its own role="status" saying it is loading,
and announcing both would make a person hear the same notice twice per fetch. Reduced motion turns the
transition off, so it jumps between widths instead of sliding, and still says the same thing: the signal is
the width, not the slide.

Seven screens traded the word "Carregando" for the shape of what is coming. The word says neither how much
nor what; the shape says three paragraphs, a card, a list, and for someone who reads with effort that
changes the decision to wait or leave. The word stays, visually hidden, because someone navigating by ear
needs the word and not the shape.

The offline strip exists because without it the person taps, nothing happens, and the screen blames our
side, which is a lie. It separates what still works from what waits: reading the explanation already on
screen works, sending answers does not.

Two fixes I made after the fact. OfflineBar started as useState in an effect and lint was right to refuse
it: it now uses useSyncExternalStore, which is the tool for subscribing to state that lives outside React
and which also answers for the server, where navigator does not exist. And RouteProgress had largura in its
dependency list, so the effect resubscribed on every width change and left the pending timeout with nobody
to clear it; a ref carries that now and the deps are empty.
@vercel

vercel Bot commented Sep 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
leia Ready Ready Preview Sep 21, 2026 1:58pm UTC

Request Review

@danielgorgonha
danielgorgonha merged commit 652b93a into main Sep 21, 2026
4 checks passed
@danielgorgonha
danielgorgonha deleted the feat/tell-the-person-what-is-happening branch September 21, 2026 14:19

This branch was successfully deployed

1 active deployment
Preview — c7c61f2f Deployed Sep 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant