Skip to content

Boas práticas do Widget Svelte ​

Tipo
Conceito
Para
Desenvolvedor

Um Widget Svelte roda em mais lugares do que o Dashboard onde foi escrito: em outro Tenant depois de instalado por template, no tema escuro, dentro do aplicativo móvel com a cor da marca, no PDF de um alerta. As regras abaixo são o que separa um Widget que funciona nesses lugares de um que funciona só na tela de quem o escreveu. São as mesmas que o assistente de IA do editor segue.

Várias das regras saem de um fato: o Widget é montado de novo a cada recarga do Dashboard, com o onMount rodando outra vez e toda variável no valor inicial (veja Como funciona).

Referencie por nome, nunca por ID ​

O código vale em qualquer Tenant só se apontar as colunas pelo nome; o motivo está no aviso de Referência de coluna.

javascript
// certo
const VALOR = '[Vendas]."VALOR":SUM';
const RECEITA = '["Receita YTD"]';
app.getDateFilterReference(app.columnFromString('[Vendas]."DATA":DATE').column_id);

// errado
const VALOR = '7905:SUM';
app.getDateFilterReference('1185933');

Guarde os nomes em constantes no topo do script. Quem for adaptar o Widget a outra Aplicação troca só ali. As três formas de nome estão em Referência de coluna.

Formate todo valor com formatValue ​

O banco devolve 1540.5 e "2026-03-15". A coluna sabe que aquilo é "R$ 1.540,50" e "15/03/2026", no idioma de quem vê. app.formatValue aplica a máscara configurada na coluna, e quem muda a máscara na Aplicação muda o Widget junto.

javascript
// certo
const texto = app.formatValue(linha[1], res.Columns[1]);

// errado: ignora a máscara, o idioma e a moeda
const texto = 'R$ ' + linha[1].toFixed(2);

Vale para rótulo de eixo, tooltip e célula de tabela. No Highcharts, use formatter chamando app.formatValue.

Trate carregando, erro e vazio ​

A consulta leva tempo, pode falhar (coluna sem acesso, tempo esgotado) e pode voltar sem linha (filtro que não casa nada). Sem os três estados, o Widget fica em branco e ninguém sabe se é bug ou ausência de dado.

svelte
<script>
  import { onMount } from 'svelte';
  export let app;
  let carregando = true, erro = null, linhas = [];

  onMount(async () => {
    try {
      const res = await app.fetchMatrix({ columns: ['[Vendas]."LOJA":AGP', '[Vendas]."VALOR":SUM'] });
      linhas = res.Result;
    } catch (e) {
      erro = e?.message ?? String(e);
    } finally {
      carregando = false;
    }
  });
</script>

{#if carregando}
  <div class="flex h-full items-center justify-center text-slate-500 dark:text-navy-300">Carregando…</div>
{:else if erro}
  <div class="flex h-full items-center justify-center text-sm text-error">{erro}</div>
{:else if !linhas.length}
  <div class="flex h-full items-center justify-center text-slate-500 dark:text-navy-300">Sem dados no período</div>
{:else}
  <!-- conteúdo -->
{/if}

Não aplique de novo os filtros do Dashboard ​

fetchMatrix e fetchData já levam os filtros da barra, da aba ativa, os cruzados e os Filtros Pré Aplicados do Widget. Repetir um filtro do Dashboard em filters não muda nada no caso comum e, na coluna de data, troca o período do Dashboard pelo seu, sem que o leitor perceba.

javascript
// certo: o período do Dashboard já está na consulta
const res = await app.fetchMatrix({ columns: [DATA, VALOR] });

// errado: congela o período que estava aplicado quando o código foi escrito
const res = await app.fetchMatrix({ columns: [DATA, VALOR],
  filters: [{ column: DATA, operator: 'between', value: '2026-01-01,2026-01-31' }] });

Passe em filters só o que é do Widget ("só pedidos faturados"). Para uma consulta que deve ignorar um filtro do Dashboard, como o total do ano ao lado do mês, use ignoreCrossfilters.

Gaste poucas consultas ​

Cada fetchMatrix é uma consulta ao banco, e ela roda de novo a cada recarga do Dashboard, para cada pessoa que o abre. Um Widget com dez consultas num Dashboard com vinte filtros trocados por dia dá centenas de consultas por pessoa.

javascript
// certo: uma consulta com as três medidas
const res = await app.fetchMatrix({ columns: [LOJA, VALOR, QTD, META] });

// errado: uma consulta por medida
const v = await app.fetchMatrix({ columns: [LOJA, VALOR] });
const q = await app.fetchMatrix({ columns: [LOJA, QTD] });
const m = await app.fetchMatrix({ columns: [LOJA, META] });

Ponha limit no que vai para a tela: um ranking de 10 não precisa das 500 linhas do padrão. Consultas que não dependem uma da outra rodam juntas com Promise.all.

Escreva Svelte 4 ​

O compilador usa Svelte 4. A sintaxe do Svelte 5 não funciona: as runes ($state, $props) dão erro ou não reagem, e onclick={fn} compila, mas vira um atributo morto e o clique não faz nada.

Svelte 4 (use)Svelte 5 (não funciona)
export let app;let { app } = $props();
let total = 0;let total = $state(0);
$: dobro = total * 2;let dobro = $derived(total * 2);
on:click={filtrar}onclick={filtrar}
<slot />{@render children()}

Deixe o tamanho com o Widget ​

O Widget já tem largura, altura e rolagem definidas pelo cartão onde está. Na raiz do componente, nunca junte h-full (altura de 100%) com overflow-auto: isso cria uma segunda barra de rolagem dentro do cartão, e na exportação para PDF e imagem o que está abaixo da dobra sai cortado.

svelte
<!-- certo: a raiz cresce com o conteúdo; dentro dela, o gráfico pode ocupar 100% -->
<div class="w-full p-4">
  <div class="h-full"><Chart {Highcharts} {options} /></div>
</div>

<!-- errado -->
<div class="h-full overflow-auto p-4">…</div>

O h-full sozinho serve para centralizar os estados de carregando e de erro. Com a opção Crescer com o conteúdo ligada, o cartão não tem altura fixa e um gráfico que depende de height: 100% some: dê altura explícita ao gráfico (chart: { height: 320 }). Para cortar o que passa da borda em vez de rolar, há a opção Cortar conteúdo que passa da borda no configurador.

Siga o tema claro, o escuro e o do aplicativo ​

O mesmo Widget aparece no tema claro, no escuro e dentro do aplicativo móvel do Lumo, que repinta a página com as cores da marca do cliente. O aplicativo repinta só as classes que a web usa. Fundo, borda e texto escritos com outra classe ou em hex ficam com a cor errada lá.

Marcação. Use as classes desta tabela. No escuro, sempre a paleta navy, nunca gray, slate ou zinc em dark:.

PapelClaroEscuro
Fundo da páginabg-slate-50dark:bg-navy-900
Cartãobg-whitedark:bg-navy-800
Painel internobg-slate-100dark:bg-navy-700
Bordaborder-slate-200dark:border-navy-600
Textotext-slate-800, text-slate-700dark:text-navy-100, dark:text-white
Texto secundáriotext-slate-500dark:text-navy-300

A cor do dado (série, vermelho e verde de status, destaque) é livre.

CSS próprio. No <style>, não use hex fixo para fundo, borda ou texto. Use a variável do aplicativo com a cor da web como reserva; fora do aplicativo a variável não existe e vale a reserva.

svelte
<div class="widget" class:dark={$app.theme === 'dark'}>…</div>

<style>
  .widget      { --superficie: var(--mt-cartao, #FFFFFF); --linha: var(--mt-linha, #E2E8F0);
                 --texto: var(--mt-texto, #1E293B); --secundario: var(--mt-texto-secundario, #64748B); }
  .widget.dark { --superficie: var(--mt-cartao, #202B40); --linha: var(--mt-linha, #313E59);
                 --texto: var(--mt-texto, #C2C9D6); --secundario: var(--mt-texto-secundario, #697A9B); }
</style>

As variáveis são --mt-fundo, --mt-cartao, --mt-rebaixado, --mt-linha, --mt-texto, --mt-texto-secundario e --mt-acao.

Gráfico. O Highcharts não lê classe do Tailwind nem variável CSS: var(--mt-linha) passado a ele vira cor nenhuma. Monte as cores em JavaScript a partir de $app.moldura, com reserva, e monte as opções do gráfico com $:, para que ele repinte quando o tema muda.

javascript
$: escuro = $app.theme === 'dark';
$: marca = $app.moldura?.ativa ? $app.moldura.cores : null;
$: cores = {
  texto:      marca?.texto ?? (escuro ? '#C2C9D6' : '#1E293B'),
  secundario: marca?.textoSecundario ?? (escuro ? '#697A9B' : '#64748B'),
  linha:      marca?.linha ?? (escuro ? '#313E59' : '#E2E8F0'),
  superficie: marca?.cartao ?? (escuro ? '#202B40' : '#FFFFFF'),
};
$: options = dados && montarOpcoes(dados, cores);

Leia datas no fuso local ​

new Date('2026-02-23') é meia-noite em UTC, que no Brasil (UTC-3) ainda é dia 22. Um calendário ou um rótulo de dia montado assim mostra o dia anterior.

javascript
// certo
const [a, m, d] = '2026-02-23'.split('-');
const dia = new Date(a, m - 1, d);         // meia-noite local
// ou
import { parseISO } from 'date-fns';
const dia2 = parseISO('2026-02-23');       // também local

// errado
new Date('2026-02-23').getDate();          // 22

Para mostrar a data, app.formatValue resolve sem esse problema.

Pense na exportação e no alerta ​

O Widget também roda sem ninguém olhando: na exportação de PDF e imagem e no envio de alerta, uma vez para cada renderização. Nesse contexto, app.user é quem disparou a exportação, não quem recebe. A exportação espera as consultas feitas pelo app antes de capturar a tela.

Daí duas regras. Não personalize o conteúdo por app.user num Widget que vai em alerta: os trinta destinatários recebem a saudação com o nome do dono do alerta. E não faça efeito colateral ao montar: chamar a API do seu ERP no onMount dispara a chamada em cada abertura, cada recarga de filtro e cada PDF. Ação que muda algo fora do Lumo vai num clique.

Guarde estado fora do componente ​

Como o componente é recriado a cada recarga, a seleção que o leitor fez num clique se perde quando esse mesmo clique filtra o Dashboard. Derive o que está selecionado do filtro, que sobrevive à recarga.

javascript
// certo: ao montar, lê o filtro que está na barra
const COL = app.columnFromString('[Vendas]."VENDEDOR":AGP');
const selecionado = app.getFilter(COL)?.value ?? null;

// errado: some na recarga que o próprio addFilter dispara
let selecionado = null;
function clicar(nome) { selecionado = nome; app.addFilter(COL, '=', nome); }

Nunca ponha segredo no código ​

O código vai inteiro para o navegador de quem abre o Dashboard (veja o que o código não pode fazer). Chave de API, senha e token fixo vazam no primeiro acesso; para falar com uma API sua, use app.generateJWTToken() (JWT). Esconder uma coluna no código também não protege nada: o que o leitor não pode ver se tira do acesso dele, no Datamart; veja Acesso a dados.

No componente paramétrico, tudo vem de config ​

Num componente paramétrico, o mesmo código serve a vários Dashboards. Coluna, título, cor e meta escritos no código obrigam a editar o componente para cada uso, e a edição publicada chega a todos. Leia de config tudo que muda de um uso para outro, e declare no contrato.

javascript
// certo
const coluna = app.columnFromString(config.valor);
const titulo = config.titulo;

// errado
const coluna = app.columnFromString('[Vendas]."VALOR":SUM');
const titulo = 'Vendas do mês';

Checklist ​

  • [ ] Toda coluna referenciada por nome, com sufixo (:AGP, :SUM); nenhum ID numérico no código
  • [ ] Todo valor mostrado passa por app.formatValue
  • [ ] Estados de carregando, erro e vazio; toda consulta dentro de try/catch
  • [ ] Nenhum filtro do Dashboard repetido em filters
  • [ ] Uma consulta com várias colunas em vez de várias consultas; limit no que vai para a tela
  • [ ] Sintaxe do Svelte 4: export let, $:, on:click
  • [ ] Raiz sem h-full junto com overflow-auto
  • [ ] Classes da tabela de tema; navy no dark:; CSS próprio com var(--mt-…, <cor da web>); cores do gráfico de $app.moldura, com $:
  • [ ] Datas lidas no fuso local
  • [ ] Nada de efeito colateral ao montar; nada personalizado por app.user em Widget de alerta
  • [ ] Seleção derivada de app.getFilter, não de variável
  • [ ] Nenhum segredo no código
  • [ ] Paramétrico: o que muda entre usos vem de config

Relacionado ​