@sevn/elections-components
v1.3.0
Published
Componentes de apuração eleitoral em tempo real. JavaScript puro, sem framework: use via import ou por uma tag <script> em HTML cru.
Readme
@sevn/elections-components
Componentes de apuração eleitoral em tempo real: mapas, rankings, progresso de urnas, comparação entre turnos. São JavaScript puro — funcionam em qualquer site, com ou sem framework, com ou sem build.
import { Core, StateLeaders } from '@sevn/elections-components';
Core.applyKey('minha-chave-aqui');
const componente = new StateLeaders({
target: document.querySelector('#placeholder-state-leaders')
});Instalação
npm install @sevn/elections-componentsNão há dependência de peer para instalar. Svelte é detalhe de implementação interno: ele vai embutido no pacote e não aparece na sua árvore de dependências.
A chave
Todo componente precisa de uma chave antes de buscar dados. Ela identifica sua CDN — é o subdomínio da API dedicada à sua conta:
Core.applyKey('minha-chave-aqui');
// → https://minha-chave-aqui-api1.sevnelections.comChame uma vez, no boot da aplicação, antes de montar qualquer componente. Sem chave o componente monta normalmente e exibe a mensagem de configuração faltando na própria área dele — nunca lança um erro que derrube sua página.
Alta disponibilidade
A cascata entre servidores é feita pelo @sevn/reqcache,
a mesma lib que cuida do cache e da deduplicação de requisições. Cada requisição
tenta, em ordem:
{chave}-api1.sevnelections.com{chave}-api2.sevnelections.comapi-failover.sevnelections.com— servidor genérico de emergência
Cada servidor é tentado uma vez: insistir no mesmo host antes de pular para
o próximo só somaria espera para quem está olhando a tela. O timeout de cada
tentativa fica por conta da lib, que o escala pela qualidade da conexão — 30s
numa rede boa, subindo até 90s em slow-2g. Dá para fixá-lo em
Core.configure({ request: { timeoutMs } }), mas raramente é boa ideia: um
valor fixo corta a resposta de quem está numa rede ruim.
O que falha fica marcado como fora do ar por 2 minutos: nesse período ele é recusado localmente, sem tocar a rede, para as requisições seguintes não pagarem o timeout dele de novo num dia de pico.
Um 404 não promove servidor. Aqui ele significa "recorte ainda não publicado" — é a resposta correta, não uma indisponibilidade —, então sobe na hora como "Dados ainda não publicados", sem repetir o mesmo caminho nos outros domínios para colher o mesmo 404.
Caindo todos os três, o último valor bom ainda em cache é servido no lugar do erro — a tela segura o número anterior em vez de zerar. Só quando não há nada em cache é que o componente exibe a falha.
O cache é compartilhado entre os hosts — trocar de servidor não invalida nada do que já foi baixado, porque a entrada fica sempre sob a URL do primário.
As três formas de usar
1. Import (Node, Bun, Deno, Vite, webpack, Next, Nuxt…)
import { Core, StateLeaders } from '@sevn/elections-components';
Core.applyKey('minha-chave-aqui');
const componente = new StateLeaders({
target: document.querySelector('#placeholder'),
year: 2026,
defaultOffice: 'governor'
});
// Quando não precisar mais dele:
componente.destroy();O pacote é tree-shakeable: importar um componente não traz os outros 24.
2. Tag <script>, em HTML cru
<div id="placeholder-state-leaders"></div>
<script src="https://cdn.sevnelections.com/components/v1/state-leaders.js?cid=minha-chave-aqui"></script>
<script>
const componente = new StateLeaders({
target: document.querySelector('#placeholder-state-leaders')
});
</script>A chave sai do ?cid= da própria tag — não há o que configurar. Cada arquivo
expõe a classe com o nome do componente (window.StateLeaders) e também
window.SevnElections.Core, se você precisar ajustar algo.
Um arquivo por componente, ~34 KB gzip cada, tudo embutido.
3. CommonJS
const { Core, StateLeaders } = require('@sevn/elections-components');API
new Componente(opções)
target é obrigatório e aceita um elemento ou um seletor CSS. As demais chaves
são as props do componente.
new StateLeaders({ target: '#ph', year: 2026, mock: true });componente.destroy()
Desmonta e remove do DOM. Devolve uma Promise, caso você precise esperar a
saída terminar. Chamar duas vezes não faz nada.
Core
| Método | Para quê |
| ------------------------------------------ | --------------------------------------------- |
| Core.applyKey(chave) | Configura a conta. Obrigatório. |
| Core.setTheme({ primary, secondary, … }) | Sobrescreve as cores. |
| Core.configure(opções) | Ajusta hosts, CDN de assets, cache, timeouts. |
| Core.getConfig() | Lê a configuração atual. |
| Core.isReady() | Diz se já há hosts resolvidos. |
| Core.activeHost() | Host que respondeu por último. Diagnóstico. |
Estilo
O CSS é injetado sozinho no momento em que o componente monta. Não há folha para linkar.
Os componentes herdam a tipografia da sua página e trazem o próprio reset
escopado às raízes sevn_* — nada do que eles definem vaza para o resto do
seu site.
As cores são custom properties com prefixo --sevn-, então você pode
sobrescrevê-las pela cascata:
:root {
--sevn-color-primary: #0a3e27;
--sevn-color-secondary: #c38214;
}Ou pela API, que dá no mesmo:
Core.setTheme({ primary: '#0a3e27', secondary: '#c38214' });Se preferir carregar os tokens antes do JS, o arquivo também é publicado:
import '@sevn/elections-components/styles.css';Props comuns
Todos os 25 componentes aceitam:
| Prop | Tipo | Padrão | Para quê |
| ---------- | ------------- | -------------- | ------------------------------------------------------------------------------------------------- |
| mock | boolean | false | Renderiza com dados de exemplo, sem tocar na API. Útil para montar o layout antes de ter a chave. |
| rawStyle | boolean | false | Remove o cartão externo (borda, fundo, respiro) para encaixar no seu layout. |
| header | HeaderProps | por componente | { title, description, tag, tagColor } — o cabeçalho exibido no topo. |
Catálogo
| Componente | Arquivo UMD | Título padrão | Props próprias |
| -------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| FinalResults | final-results.js | Presidente | showAd: boolean = trueyear: number = 2026round: number = 1 |
| StateWinners | state-winners.js | Quem ganhou em cada estado | year: number = 2026round: number = 1defaultUf: string \| null |
| VoteCountingProgress | vote-counting-progress.js | Evolução da apuração | year: number = 2026round: number = 1 |
| VoteComparisonByRound | vote-comparison-by-round.js | Comparação de votos entre 1º e 2º turno | defaultOffice: 'president' \| 'governor' = 'president'defaultUf: string \| null |
| CountingProgress | counting-progress.js | Progresso da Apuração | year: number = 2026defaultOffice: 'president' \| 'governor' = 'president'round: number = 1defaultUf: string \| null |
| StateLeaders | state-leaders.js | Veja quem lidera em cada estado | year: number = 2026defaultOffice: 'president' \| 'governor' = 'president' |
| CurrentLeader | current-leader.js | Liderança Atual | year: number = 2026round: number = 1 |
| TopDispute | top-dispute.js | Disputa no topo | year: number = 2026 |
| CandidateRanking | candidate-ranking.js | Posições de liderança | year: number = 2026 |
| LiveSummary | live-summary.js | Resumo em tempo real | year: number = 2026round: number = 1 |
| CandidatePerformanceByMunicipality | candidate-performance-by-municipality.js | Consulte o desempenho dos candidatos por município | year: number = 2026defaultOffice: 'president' \| 'governor' = 'president'defaultUf: string \| nulldefaultMunicipality: string \| nulldefaultRound: '1turno' \| '2turno' \| '' = '' |
| RoundResultsAnalysis | round-results-analysis.js | Análise de resultados por turno | year: number = 2026defaultOffice: 'president' \| 'governor' = 'president'defaultUf: string \| nulldefaultMunicipality: string \| null |
| ElectionStatus | election-status.js | Situação eleitoral | year: number = 2026defaultOffice: 'president' \| 'governor' = 'president'defaultUf: string \| nulldefaultMunicipality: string \| null |
| CandidateResultsByMunicipality | candidate-results-by-municipality.js | Consulte o desempenho dos candidatos por município | year: number = 2026 |
| MunicipalityPerfomanceMap | municipality-perfomance-map.js | Consulte o desempenho dos candidatos por município | round: number = 1year: number = 2026 |
| CandidateByState | candidate-by-state.js | Governador por Estado | round: number = 1year: number = 2026 |
| CandidateCarrouselStateResultsViewer | candidate-carrousel-state-results-viewer.js | Governador | year: number = 2026office: 'president' \| 'governor' = 'governor'defaultUf: string \| null |
| CandidateDuelStateResultViewer | candidate-duel-state-result-viewer.js | Governador | round: number = 1year: number = 2026showPosition: boolean = falsepositionTagColor: stringdefaultUf: string \| null |
| CandidateMapByState | candidate-map-by-state.js | Governador por Estado | year: number = 2026round: number = 1 |
| ElectionRoundAnalysis | election-round-analysis.js | Análise de resultados por turno | year: number = 2026defaultUf: string \| nulldefaultOffice: 'president' \| 'governor' = 'governor' |
| CandidateMultiRingChartPerformanceByMunicipality | candidate-multi-ring-chart-performance-by-municipality.js | Consulte o desempenho dos candidatos por município | year: number = 2026defaultUf: string \| nulldefaultMunicipality: string \| nulldefaultOffice: 'president' \| 'governor' \| 'senator' = 'governor' |
| ResultsByOffice | results-by-office.js | Resultados por cargo | year: number = 2026defaultOffice: 'senator' \| 'federal_deputy' \| 'state_deputy' = 'senator'itemsPerPage: number = 10defaultUf: string \| nulldefaultMunicipality: string \| null |
| VotingMap | voting-map.js | Mapa de Votação | showAd: boolean = falseyear: number = 2026round: number = 1defaultUf: string \| nulldefaultMunicipality: string \| nulldefaultOffice: 'president' \| 'governor' \| 'senator' = 'president' |
| CandidateCountingProgress | candidate-counting-progress.js | Evolução da Apuração para Governador | year: number = 2026defaultUf: string \| null |
| PartyPerformance | party-performance.js | Desempenho por Partido (senador/deputado) | year: number = 2026defaultUf: string \| nulldefaultOffice: 'federal_deputy' \| 'state_deputy' \| 'senator' = 'federal_deputy' |
Limitações conhecidas
Os tamanhos internos usam rem, que resolve contra o font-size do <html>
da sua página. Se você usa a técnica de html { font-size: 62.5% }, os
componentes vão encolher junto. Enquanto isso não muda, contorne isolando o
container:
#placeholder {
font-size: 16px;
}
html {
font-size: 100%;
}Desenvolvimento
Este repositório contém tanto a biblioteca quanto o showcase que a demonstra.
bun install
bun run dev # showcase em http://localhost:4321
bun run check # svelte-check
bun run test # testes unitários (transporte, config)Empacotamento:
bun run build:pkg # ESM + CJS + tipos + estilos + 25 bundles UMD
bun run test:types # type-check de um consumidor contra o dist publicado
bun run serve:test # sobe test/umd.html para exercitar o UMD no navegador
bun run build:cdn # monta dist-cdn/ na estrutura do R2Os componentes são escritos em Svelte 5 (src/components), sobre primitivos
internos (src/lib). Nada de src/lib é público: mudanças ali não são
breaking changes. O que o consumidor vê é src/index.ts — classes JS geradas
por defineComponent, com as props tipadas em src/components/props.ts.
O showcase é SvelteKit, mas o pacote não é: vite.lib.config.ts e
scripts/build-umd.mjs não usam o plugin do SvelteKit. O @sveltejs/package
não é usado em lugar nenhum — ele não bundla, só copia .svelte, e era
exatamente isso que obrigava o consumidor a ter Svelte.
Publicação
npm publish # prepublishOnly roda build, testes e publintDeploy para o R2
cp .env.example .env # preencha as chaves do R2 (abaixo)
bun run deploy:cdn # build dos UMD -> dist-cdn/ -> upload -> purge do edgedeploy:cdn fala a API S3 do R2 direto, compara o MD5 local com o ETag remoto e
sobe só o que mudou — os ~5.600 mapas ficam parados depois da primeira vez.
Variações:
bun run deploy:cdn:dry # mostra o diff sem enviar nada
bun run deploy:cdn:components # só os bundles UMD (o caso comum)
bun run cdn:cors # aplica r2-cors.json, uma vez sóAs chaves
Duas, no .env (que é gitignored):
1. Token do R2, para o upload. Dashboard → R2 Object Storage → API → Manage API tokens → Create API token
- Permissão: Object Read & Write
- Escopo: Apply to specific buckets → o bucket de produção
- Copie Access Key ID e Secret Access Key. O Account ID é o subdomínio do endpoint S3, que aparece em Settings do bucket.
2. Token de purge (opcional), para invalidar o edge na hora. Dashboard → My Profile → API Tokens → Create Token → Custom token
- Permissão: Zone → Cache Purge → Purge
- Zone Resources: Include → Specific zone →
sevnelections.com - O Zone ID está no Overview da zona.
Sem o segundo par o deploy funciona igual — o edge só se atualiza sozinho em até
s-maxage em vez de imediatamente.
Cache: o que é imutável e o que não é
Os dois tipos de arquivo têm ciclos de vida opostos, e tratá-los igual era um bug
real: os bundles UMD subiam immutable num caminho que é sobrescrito a cada
release, o que prendia o navegador do cliente na versão antiga por até um ano,
sem nem revalidar no reload.
| Caminho | Muda no mesmo endereço? | cache-control |
| ------------------------------- | ----------------------------- | -------------------------------------------------- |
| static/v1/** | Não — mudar é publicar /v2/ | max-age=31536000, immutable |
| components/releases/<id>/*.js | Nunca | max-age=31536000, immutable |
| components/v1/*.js | Sim, a cada release | max-age=300, s-maxage=60, stale-while-revalidate |
components/v1/<nome>.js é o endereço colado no HTML do cliente: precisa
continuar estável e poder mudar de conteúdo. O navegador segura 5 minutos e o
edge 60 segundos; passado isso, o stale-while-revalidate serve a cópia velha na
hora e busca a nova em segundo plano, então a expiração nunca custa latência para
quem está acompanhando apuração.
Todo deploy também arquiva uma cópia imutável em
components/releases/<id>/<nome>.js, com <id> derivado do conteúdo dos
bundles. É o que permite fixar uma versão ou voltar atrás sem depender do alias.
Em dia de apuração, para encurtar a janela:
CDN_ALIAS_MAX_AGE=30 CDN_ALIAS_S_MAXAGE=10 bun run deploy:cdnO embed do cliente leva ?cid=, e a Cloudflare cacheia por URL completa — o que
faria o purge acertar só a URL sem query. Por isso a zona tem uma Cache Rule,
Componentes UMD: ignora query string na chave de cache, em
/components/*: todas as variantes ?cid= compartilham uma entrada, então um
purge cobre todos os clientes. É seguro porque a cid é lida no navegador, a
partir da própria tag <script> — o arquivo servido não varia com a query.
Essa mesma zona tem uma regra anterior (Cache JSON files) que aplica
edge_ttl: override_origin 300 em todo o host da CDN. Na prática o edge segura
300s em vez dos 60s do s-maxage; o purge no fim do deploy é o que torna isso
irrelevante.
Se algum arquivo em static/v1/** mudar de conteúdo no mesmo caminho, o deploy
avisa — quem já baixou não verá a versão nova. O caminho certo nesse caso é subir
o assetsVersion em src/core/config.ts (v1 → v2).
Fallback pelo wrangler
scripts/upload-cdn.mjs continua existindo para quando não há chave S3 à mão:
usa wrangler r2 object put (autenticação por wrangler login) e um manifesto
local para retomar de onde parou. É bem mais lento — um processo Node por objeto
— mas escreve os mesmos cabeçalhos, porque os dois scripts compartilham
scripts/cdn-policy.mjs.
wrangler login
bun run build:cdn
node scripts/upload-cdn.mjs --bucket=SEU_BUCKET --dry-run
node scripts/upload-cdn.mjs --bucket=SEU_BUCKETLicença
MIT
