feat(frontend): reforma visual, arquitetural e de performance - #2
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
O que é
Reforma completa do frontend (
static/+ serving emapp.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 noinput(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,ACTIONSmap,registerEvents(), gzip + CSP + cache,components-optional.csse95851e— link dinâmico do card de atenção, toast com fio de tone, btn com contagem, toast pausa no hover, preload + fontes versionadas976cd94—text-wrap: balance, O/CO na confirmação (cita backup), saídas em becos-sem-saída, constantes de tempoSkills aplicadas
ux-heuristics,refactoring-ui,microinteractions,high-perf-browser,frontend-design,web-typography,cro-methodologyGate (todos verdes)
pytest tests/— 169 passednode --test tests/js/— 38/38ruff check,pyright— limposnode --check static/app.js— OKRiscos e notas
GZipMiddlewaree headers novos não tocam rotas nemresponse_model;test_contract.py(inclui snapshot OpenAPI) passa.components-optional.cssnão é carregado por padrão (textarea/select/checkbox/tooltip/drawer/dialog/menu não usados nas 3 telas).