Skip to content

feat(frontend): reforma visual, arquitetural e de performance - #2

Merged
yanhenrique-dev merged 6 commits into
masterfrom
feat/frontend-overhaul
Sep 30, 2026
Merged

yanhenrique-dev merged 6 commits into
masterfrom
feat/frontend-overhaul

Conversation

@yanhenrique-dev

Copy link
Copy Markdown
Owner

O que é

Reforma completa do frontend (static/ + serving em app.py): fecha os 6 achados de alta da análise (docs/ANALISE-FRONTEND.md), aplica 7 skills e não muda nenhum formato de resposta (contrato intacto).

Commits

  • e22e98f — contraste success (A3), foco dos switches (A1), anel de foco por tema (A4), foco do viewport (M6)
  • dd8beb0 — região viva única (A2/M2), validação no input (M3), rascunho sobrevive ao re-render (A5), update em lote cirúrgico (M8), contagem em 1 passe (M9)
  • 804b8db — opt-out de atalhos (A6), roving tabindex (M1), tabela focalizável (M5), sidebar com foco (M4), scroll rAF (M7), aria-busy (B13)
  • 42a4c5b — stat-cards que filtram, steps com progresso real, thead sticky, ACTIONS map, registerEvents(), gzip + CSP + cache, components-optional.css
  • e95851e — link dinâmico do card de atenção, toast com fio de tone, btn com contagem, toast pausa no hover, preload + fontes versionadas
  • 976cd94 — text-wrap: balance, O/CO na confirmação (cita backup), saídas em becos-sem-saída, constantes de tempo

Skills aplicadas

ux-heuristics, refactoring-ui, microinteractions, high-perf-browser, frontend-design, web-typography, cro-methodology

Gate (todos verdes)

  • pytest tests/ — 169 passed
  • node --test tests/js/ — 38/38
  • ruff check, pyright — limpos
  • node --check static/app.js — OK

Riscos e notas

  • GZipMiddleware e headers novos não tocam rotas nem response_model; test_contract.py (inclui snapshot OpenAPI) passa.
  • components-optional.css não é carregado por padrão (textarea/select/checkbox/tooltip/drawer/dialog/menu não usados nas 3 telas).
  • Ficaram de fora: B11 (chave de confirmação por nome, só teórica), B14 (tr extra do map-form), I2 (busy global → contador; muda concorrência, PR separado).

A3 — `--v2-state-fg-success` era green-800 (#198b43) a 3,97:1 sobre o
bg-success, em texto de 11px (tag "Em dia") e 13px (.status-line): falha
WCAG 1.4.3. green-900 (#1d783c) mede 5,03:1 no bg e 4,93:1 em layer-02,
então o token também segura se a tag renderizar sobre superfície neutra.

A1 — a regra de foco do switch era
`[data-component="switch"] [data-slot="switch-control"]:focus-visible`, e o
span filho não é focalizável: a regra nunca casava e os três switches
(Detecção automática, Modo, Backup) estavam sem indicador de foco, falha
WCAG 2.4.7. A regra vai para o <button>, que é o alvo real.

A4 — `--v2-border-border-focus` era blue-500 (#7698fd) nos dois temas:
2,73:1 em #fff, 2,62:1 em layer-01, 2,44:1 em layer-02. Fica blue-700 no
light (6,27/6,01/5,60) e blue-300 no dark (12,20/10,46). O anel é
desenhado com outline-offset, ou seja fora do fill — o fundo que importa é
o da superfície, não o do botão contrast, que é o que faz blue-700
funcionar no light apesar de o bg-contrast ser escuro.

M6 — o viewport do scroll-view é focalizável (é a única forma de rolar a
região, já que a scrollbar nativa está escondida) mas tinha `outline: none`
sem reposição. WCAG 2.4.7.

Robustez — `--v2-grey-600` usava a keyword `gray`, alias de `grey`:
virou hex explícito, sem mudar o valor resolvido.

STYLEGUIDE: a tabela de tokens estava divergindo do código em cinco
pontos (faint, warning, success, focus e a ordem de carga) e mandava usar
script inline para o tema, o que a CSP proíbe. Registra os desvios
conscientes da fonte original, com a razão e a medição de cada um.
… ao render

A2 — o unico live region da app era #status, que mora dentro de
#library-view; setView() faz libraryView.hidden = true, entao em Atividade
e Preferencias nada era anunciado — inclusive as mensagens dos switches e
os erros de operacao. Agora #live-status (sr-only) vive fora das <section
hidden>, dentro de <main>, e say() espelha la. announce() limpa e reescreve
no frame seguinte, senao "Acao cancelada." duas vezes seguidas so falaria
uma.

M2 — sobravam quatro live regions concorrentes (.connection-card,
#sync-state e as tres tags de switch), todas interrompindo o leitor varias
vezes numa operacao. Removidas: o app passa a ter uma regiao viva so.
O switch nao perde nada — role="switch" + aria-checked ja anunciam a
mudanca de estado nativamente. #status continua visual, sem aria-live,
senao anunciaria em duplicidade na view Biblioteca.

M3 — a validacao do caminho rodava no blur, quando o foco ja tinha saido do
campo: o erro aparecia exatamente no caso que o leitor de tela nao alcanca.
Agora valida no input, com #path-error como role="alert". O blur ficou so
para o autofill do navegador, que nao dispara input, e sem reanunciar.

A5 — render() trocava o innerHTML do <tbody> inteiro e destruia o <input>
de URL junto com o que o usuario tinha digitado, com o foco caindo no
<body> e o Tab recomecando do topo. Disparava em digitar na busca (debounce
de 150 ms), trocar filtro, setBusy e update. O beforeunload que existe para
avisar texto nao salvo so cobre unload, nao re-render — a protecao nao
cobria o caso real. captureUrlDrafts/restoreUrlDrafts chaveiam o rascunho
pelo row.name, nao pelo indice: indexado, um filtro que reordene a tabela
jogaria a URL digitada na linha errada e "Salvar" gravaria no repo errado.

M8 — no modo em lote cada doUpdate pintava 3x, e cada pintura reescrevia as
N linhas (60 repos = ~180 reconstrucoes, com a rolagem pulando e o botao
"Atualizar pendentes" perdendo o foco a cada passo). O render do meio foi
eliminado — era seguido imediatamente pelo do finally, sem nada visivel no
meio — e o caminho quiet passou a trocar so o <tr> da linha, com queda para
o render completo quando a linha nao esta na vista.

M9 — countRows() rodava 5 .filter() completos sobre state.rows todo render,
e o resultado era jogado fora por quem so precisa dos totais. Agora e um
passe so.

Testes: o shim nao fazia parse de HTML, entao innerHTML nao criava elements
e nao dava para testar o que o render faz. O harness agora recria os inputs
[data-url-input] a partir do html e conta reconstrucoes do <tbody> — e
passou a ser ele quem pega a regressao. requestAnimationFrame tambem foi
adicionado ao shim. Os testes novos falham sem as correcoes: verifiquei
desligando cada fix.
…ebar

A6 — atalhos E/C agora respeitam Preferências (switch "Atalhos de teclado"
persistido em alldown.shortcuts), ignoram isContentEditable e event.repeat,
e exigem que o opt-out esteja ligado. WCAG 2.1.4.

M1 — filtros como radiogroup com roving tabindex (um único tab stop, os
demais tabindex=-1), Home/End + ArrowLeft/Right. syncFilterTabIndex
chamado a cada render.

M4 — sidebar off-canvas move foco ao abrir, devolve ao gatilho ao fechar,
reseta data-open ao voltar para desktop e usa z-index 45 para não ficar
atrás do scrim.

M5/M6 — .table-scroll focalizável (tabindex+role=region) com outline em
:focus-visible; scroll-thumb com rAF e coalescing para não forçar layout a
cada mutação.

B1/B2 — stat-card usa tokens (--v2-text-stat-*) e log usa 60dvh.
B13 — aria-busy na tabela durante setBusy.
M10 — storeSet da busca dentro do debounce.

Co-authored-by: internal-model
Visual/UX:
- stat-cards viram botões que filtram a lista (data-filter-link + data-active)
- steps 01/02/03 refletem o progresso real (paintSteps: pasta, check, tudo em dia)
- thead sticky com fundo da camada; breadcrumb com aria-current
- map-form em <form>: Enter salva, submit delegado (antes só o clique)
- forced-colors: foco via Highlight, status-dot sem ajuste forçado

Código (app.js, sem build, mesmo contrato):
- handleAction (cadeia de ifs) vira tabela ACTIONS + helpers + try/catch
- todos os listeners em registerEvents() chamado pelo init (B5)
- despachante único de clique com guarda barata (B8/B9: 1× closest)
- cancelPendingScan() em todos os gatilhos de scan (B10)
- focusScan via rAF, sem setTimeout frágil (B12)
- render() com error boundary (I5); FILTER_INFO sem label duplicado (B4)

Performance/serving (app.py):
- GZipMiddleware (minimum 500B): ~100KB CSS+47KB JS -> ~15KB
- CSP completa (object-src/base-uri/frame-ancestors/form-action none/self)
  + Permissions-Policy; Cache-Control immutable para ?v=
- ?v= unificado em v3 nos 5 assets (B16)
- components-optional.css: textarea/select/checkbox/tooltip/drawer/dialog/menu
  fora do crítico (~7KB gzip poupados, B6); tokens duplicados removidos (B3)
- stat-card em tokens, log em dvh, aria-busy na tabela (B1/B2/B13)

Gate: 169 pytest + 38 node --test + node --check + ruff + pyright verdes.
…teractions, high-perf-browser

ux-heuristics (H4/H6, Krug #3):
- card "Revisão ou erro" somava 2 categorias mas linkava 1: link dinâmico
  (paintSteps aponta p/ sem-dono ou erros, conforme a não-vazia)
- branch explícita com title="Branch fixada no mapeamento" (H6)
- cabeçalhos com metade das palavras (Biblioteca/Atividade/Preferências)

refactoring-ui:
- toast com fio lateral no tone (hierarquia sem ler o ícone)
- map-row com fio de pertencimento à linha do repo

microinteractions:
- "Atualizar pendentes (N)": botão mostra o tamanho do lote
- toast pausa no hover, rearma 2s na saída (não some sob o mouse)

high-perf-browser:
- preload de app.js + color-scheme dark light
- fontes com ?v=3 (casam com o preload; caem no immutable do servidor)

Gate: 169 pytest + 38 node --test + node --check + ruff + pyright verdes.
web-typography (diagnóstico 8/10):
- text-wrap: balance nos títulos de página e empty-state; pretty no corpo
- medida já em 68ch/32ch; payload 72KB < 200KB; swap + fallbacks ok
- exceção documentada: corpo 13px é densidade de ferramenta, não leitura

cro-methodology (O/CO no ponto de fricção):
- confirmação do update cita o backup ("Com backup para reversão."
  ou "Sem backup: sem reversão.") — o medo de perder arquivos
  responde-se onde nasce, não nas Preferências
- empty de pasta vazia ganha saída ("Escolher outra pasta")
- sugestão vazia indica o próximo passo ("Cole a URL manualmente.")

clean-code:
- tempos mágicos viram constantes (SEARCH_DEBOUNCE_MS,
  PATH_AUTO_SCAN_DELAY_MS, TOAST_LIFETIME_MS, TOAST_HOVER_LIFETIME_MS,
  LOG_POLL_MS, MAX_TOASTS) — zero mudança de comportamento

Gate: 169 pytest + 38 node --test + node --check + ruff + pyright verdes.
@yanhenrique-dev
yanhenrique-dev merged commit c4197c8 into master Sep 30, 2026
7 checks passed
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