Aller au contenu
← Retour aux projets

Filament Odometer Easy

filament-odometer-easy

Latest Version Total Downloads License

#Filament Odometer Easy

🇧🇷 Português · 🇺🇸 English

Contadores animados para o Filament v3, v4 e v5 — tabelas, infolists e widgets de estatísticas — do jeito mais simples possível: instale, registre o plugin e use.

É o mesmo efeito do contador "Items found" da página oficial filamentphp.com/plugins, pronto para os seus dashboards e métricas em tempo real.

#🎬 Demo

OdometerStat no dashboard — com poll, os contadores re-animam sozinhos a cada atualização de valor:

OdometerStat com polling

OdometerColumn em tabelas — animação no load, na ordenação e na troca de página:

OdometerColumn em tabela

OdometerEntry em infolists e OdometerNavigationBadge em menus:

OdometerEntry em infolist e navigation badge

Badge visível com a sidebar recolhida — o Filament esconde o badge quando o menu recolhe; com ->badgeOnCollapsedSidebar() ele passa a flutuar no canto do ícone, no mesmo formato do botão de filtros da tabela:

Claro Escuro
Badge na sidebar recolhida Badge na sidebar recolhida, modo escuro

#Componentes

Componente Estende Uso
OdometerColumn TextColumn Colunas de tabela
OdometerEntry TextEntry Entries de infolist
OdometerStat Stat Counts no StatsOverviewWidget
OdometerNavigationBadge Badge de navegação (getNavigationBadge()) — com opção de continuar visível com a sidebar recolhida
Facade FilamentOdometerEasy Qualquer view/blade customizado

Todos herdam 100% da API do componente base (sortable, searchable, label, description, color etc.) — só o valor passa a ser animado.

#Motores de animação (drivers)

O pacote traz dois motores e você escolhe por config ou de forma fluente no plugin:

#number-flow — padrão ⭐

O web component number-flow (usado pelo próprio site do Filament):

  • Zero dependências — sem jQuery, sem CDN; o bundle (~16 KB) já vem no pacote
  • Anima do 0 no primeiro render — exibe 0 e, após um delay configurável, anima até o valor
  • Re-anima a cada atualização — perfeito com Livewire, poll() e dashboards em tempo real
  • Formatação nativa via Intl.NumberFormat — moeda, decimais e locale (pt-BR1.000,00)
  • Acessível — respeita prefers-reduced-motion
  • ✅ Mantido ativamente

#odometer — secundário

O efeito clássico do odometer.js via gsferro/laravel-odometer-easy (instalado como dependência):

  • 🎨 7 temas visuais: default, car, digital, minimal, plaza, slot-machine, train-station
  • ⚠️ Depende do jQuery (o plugin injeta automaticamente no <head> dos painéis)
  • ⚠️ Anima apenas na primeira renderização (não re-anima ao atualizar o valor)

#Compatibilidade

Filament Suporte Observações
5.x
4.x
3.x (3.2+)

A mesma versão do pacote atende as três — o Composer resolve pela versão do Filament do seu projeto. Requer PHP 8.2+.

#Instalação

composer require gsferro/filament-odometer-easy
php artisan filament:assets

Registre o plugin no seu painel:

use Gsferro\FilamentOdometerEasy\FilamentOdometerEasyPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->plugin(FilamentOdometerEasyPlugin::make());
}

Pronto. ✨ Sem npm, sem publicar views, sem configurar assets — o driver number-flow já funciona.

[!TIP] A maioria das apps já roda filament:assets automaticamente no post-autoload-dump (via filament:upgrade). Nesse caso, basta o composer require.

#Uso

#Coluna de tabela

use Gsferro\FilamentOdometerEasy\Tables\Columns\OdometerColumn;

OdometerColumn::make('total_vendas')
    ->label('Total de vendas')
    ->sortable(),

#Entry de infolist

use Gsferro\FilamentOdometerEasy\Infolists\Components\OdometerEntry;

OdometerEntry::make('total_vendas')
    ->label('Total de vendas'),

#Stat (StatsOverviewWidget)

use Gsferro\FilamentOdometerEasy\Widgets\OdometerStat;

protected function getStats(): array
{
    return [
        OdometerStat::make('Total de vendas', Venda::count())
            ->description('Últimos 30 dias')
            ->descriptionIcon('heroicon-m-arrow-trending-up')
            ->color('success'),
    ];
}

[!TIP] Combine com ->poll('10s') no widget: com o driver number-flow, o contador re-anima a cada atualização de valor. 📈

#Badge de navegação (menu do painel)

use Gsferro\FilamentOdometerEasy\Navigation\OdometerNavigationBadge;

// no Resource (ou Page)
public static function getNavigationBadge(): ?string
{
    return OdometerNavigationBadge::make(static::getModel()::count());
}

// ou em um NavigationItem customizado
NavigationItem::make('Vendas')
    ->badge(fn (): string => OdometerNavigationBadge::make(Venda::count())),

A API de navegação do Filament só aceita string (HTML é escapado), então o componente envolve o valor com um marcador invisível e o JS do pacote troca o texto do badge por um <number-flow> animado. A formatação usa a config global do number-flow (locales, format, delay, duration).

[!NOTE] Disponível apenas no driver number-flow. No driver odometer, o valor é exibido como texto puro, sem animação.

#Mantendo o badge visível com a sidebar recolhida

Com ->sidebarCollapsibleOnDesktop() no painel, o Filament esconde o badge assim que a sidebar recolhe: o container carrega x-show="$store.sidebar.isOpen" e ganha display:none inline. A contagem some justamente no modo em que só há ícone — o modo com menos informação.

A opção vive no plugin, dentro do seu Panel Provider — e depende de o painel ter a sidebar recolhível, que é o estado que ela cobre:

// app/Providers/Filament/AdminPanelProvider.php

public function panel(Panel $panel): Panel
{
    return $panel
        ->id('admin')
        ->path('admin')
        // 1. pré-requisito: sem sidebar recolhível não existe o estado a corrigir
        ->sidebarCollapsibleOnDesktop()
        ->plugin(
            FilamentOdometerEasyPlugin::make()
                // 2. mantém o badge visível quando ela recolhe
                ->badgeOnCollapsedSidebar()
        );
}

[!WARNING] Sem ->sidebarCollapsibleOnDesktop() (ou ->sidebarFullyCollapsibleOnDesktop()) no painel, a opção não faz nada: o Filament nunca entra no estado recolhido, e o CSS só age em .fi-main-sidebar:not(.fi-sidebar-open).

O badge passa a flutuar no canto superior direito do ícone, com fundo sólido recortando a borda — exatamente o formato que o Filament já usa no gatilho de filtros da tabela. Com a sidebar aberta, nada muda: o layout nativo (badge em linha, à direita do rótulo) é preservado.

  • ✅ Só CSS — nenhuma view do Filament publicada, nenhum JavaScript
  • ✅ Inline no <head> (~600 bytes) — não exige php artisan filament:assets
  • ✅ Vale para os dois drivers: é posicionamento do badge do Filament, não do contador
  • ✅ Modo claro e escuro, e RTL

[!IMPORTANT] Opt-in. Fica desligado por padrão para que atualizar a versão não mude a aparência do menu de quem não pediu. Para ligar via config: 'badge-on-collapsed-sidebar' => true.

[!TIP] A folga à direita do item é de ~16px, então contagens de 5+ dígitos podem perder 1-2px na borda (.fi-sidebar-nav é overflow-x:hidden). Se for o seu caso, use notação compacta: ->format(['notation' => 'compact']) — 12.345 vira 12K.

#Em qualquer view (facade)

use Gsferro\FilamentOdometerEasy\Facades\FilamentOdometerEasy;

// driver configurado (number-flow por padrão)
FilamentOdometerEasy::render(1500);

// forçando um driver pontualmente
FilamentOdometerEasy::renderNumberFlow(1500, format: ['style' => 'currency', 'currency' => 'BRL']);
FilamentOdometerEasy::renderOdometer(1500, format: '(.ddd),dd', class: 'h3');

#Formatação

O método ->format() está disponível em todos os componentes e aceita o formato do driver ativo. No driver number-flow (padrão), passe um array com opções do Intl.NumberFormat — a formatação (símbolo, separadores, casas decimais) é aplicada pelo navegador, animada dígito a dígito.

#Moeda (R$, US$, €…)

Por padrão o contador exibe apenas o número. Para mostrar o símbolo da moeda, passe um format com style: currency:

OdometerStat::make('Valor aprovado (projetos em andamento)', $aprovado)
    ->format(['style' => 'currency', 'currency' => 'BRL']),

[!TIP] Combine com ->locales('pt-BR') no plugin (ou na config) para obter R$ 1.234,56 — sem locale, o navegador do usuário decide os separadores.

#Receitas prontas (driver number-flow)

Resultado (pt-BR) ->format([...])
R$ 1.234,56 (moeda) ['style' => 'currency', 'currency' => 'BRL']
R$ 1.235 (moeda sem centavos) ['style' => 'currency', 'currency' => 'BRL', 'maximumFractionDigits' => 0]
US$ 1.234,56 / € 1.234,56 ['style' => 'currency', 'currency' => 'USD'] / 'EUR'
12,5% (percentual) ['style' => 'percent', 'minimumFractionDigits' => 1]
1.234,50 (decimais fixos) ['minimumFractionDigits' => 2, 'maximumFractionDigits' => 2]
1,2 mi (notação compacta) ['notation' => 'compact']
1.234 km (unidades) ['style' => 'unit', 'unit' => 'kilometer']
+1.234 (sinal sempre visível) ['signDisplay' => 'always']
1234 (sem agrupamento) ['useGrouping' => false]

[!WARNING] style: percent multiplica o valor por 100 — passe 0.125 para exibir 12,5%.

#Formato dinâmico (Closure)

->format() também aceita Closure. Em colunas e entries, o Filament injeta $record/$state:

OdometerColumn::make('saldo')
    ->format(fn (Conta $record): array => [
        'style' => 'currency',
        'currency' => $record->moeda, // BRL, USD, EUR...
    ]),

#Velocidade da animação

Todos os componentes aceitam ->duration() (driver number-flow; quanto maior, mais lento):

OdometerStat::make('Receita', $total)
    ->duration(2000), // conta em câmera lenta ✨

#Driver odometer

No driver secundário, ->format() recebe a string data-format do odometer.js:

OdometerColumn::make('receita')
    ->format('(.ddd),dd'),

#Onde configurar cada opção do number-flow

Opção Por componente Global (plugin/config) O que faz
format ->format([...]) ->format([...]) Opções do Intl.NumberFormat (moeda, percentual, decimais…)
duration ->duration(ms) ->duration(ms) Velocidade da animação (padrão ~900ms)
locales ->locales('pt-BR') Idioma/separadores (1.000,00)
delay ->delay(ms) Espera antes da animação inicial 0 → valor (padrão 500ms)

O valor por componente sempre vence o global. A facade FilamentOdometerEasy::renderNumberFlow() aceita todas as opções por chamada (format, delay, duration).

Referências: opções do Intl.NumberFormat · format do odometer.

#Configuração

#Fluente, direto no plugin

FilamentOdometerEasyPlugin::make()
    ->locales('pt-BR')                                      // number-flow: 1.000,00
    ->format(['style' => 'currency', 'currency' => 'BRL'])  // padrão global
    ->delay(500)                                            // ms antes da animação inicial (0 → valor)
    ->duration(1500)                                        // velocidade da animação em ms (padrão ~900ms)
    ->badgeOnCollapsedSidebar(),                            // badge do menu visível com a sidebar recolhida

Para usar o motor clássico:

FilamentOdometerEasyPlugin::make()
    ->driver('odometer')
    ->theme('digital')          // default, car, digital, minimal, plaza, slot-machine, train-station
    ->format('(.ddd),dd')       // data-format padrão
    ->jquery(enabled: false),   // quando a aplicação já carrega o jQuery

#Ou pelo arquivo de config

php artisan vendor:publish --tag="filament-odometer-easy-config"
return [
    // number-flow (padrão) | odometer
    'driver' => 'number-flow',

    // mantém o navigation badge visível com a sidebar recolhida no desktop
    'badge-on-collapsed-sidebar' => false,

    'number-flow' => [
        'locales' => null,  // ex.: 'pt-BR'; null usa o locale do navegador
        'format' => null,   // ex.: ['style' => 'currency', 'currency' => 'BRL']
        'delay' => 500,     // ms antes da animação inicial: exibe 0 e anima até o valor
        'duration' => null, // velocidade da animação em ms; null usa o padrão (~900ms)
    ],

    'odometer' => [
        'theme' => 'default',
        'format' => null,  // ex.: '(.ddd),dd'; null usa o padrão (pt-BR: 1.000,00)

        'jquery' => [
            'enabled' => true,
            'src' => 'https://code.jquery.com/jquery-4.0.0.min.js',
            'integrity' => 'sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=',
        ],
    ],
];

#Como funciona por baixo dos panos

  • number-flow: o pacote já entrega o web component <number-flow> bundlado (resources/dist/filament-odometer-easy.js, registrado como ES module via FilamentAsset), o mesmo usado em filamentphp.com/plugins. A view Blade renderiza o elemento com data-value/data-format/data-locales e o bundle o inicializa: exibe 0, espera o delay e anima até o valor. Um MutationObserver acompanha as mudanças de data-value feitas pelo morph do Livewire (poll, refresh) e re-anima do valor atual para o novo — sem depender de x-init, que não roda de novo quando o Livewire preserva o elemento.
  • Navigation badge: getNavigationBadge() e NavigationItem::badge() são tipados como ?string e o Blade escapa o conteúdo, então não dá para retornar HTML. OdometerNavigationBadge::make() envolve o valor com U+2060 (word joiner, invisível); o bundle detecta o marcador no .fi-badge-label, troca o texto por um <number-flow> e usa a config global exposta em window.filamentOdometerEasy por render hook. Quando o Livewire re-renderiza o badge, a animação parte do valor anterior (data-start).
  • Badge com a sidebar recolhida: não há prop, config nem render hook por item no Filament para isso, e publicar a view sidebar.item congelaria 150 linhas de Blade a cada upgrade. O pacote injeta um <style> no <head> por render hook, escopado em .fi-main-sidebar:not(.fi-sidebar-open) — o próprio Filament já expõe o estado da sidebar como classe (fi-sidebar-open) e o item de menu já é position: relative. O display: flex !important é o que vence a declaração inline que o x-show do Alpine escreve. Só dentro de @media (width >= 64rem), o mesmo breakpoint do store do Alpine do Filament.
  • odometer: os assets (tema css, odometer.js, odometer-easy.js) são servidos direto do vendor do gsferro/odometer-easy via FilamentAsset, e o jQuery é injetado por render hook no <head> dos painéis.
  • A troca de driver seleciona quais assets são registrados — nunca os dois ao mesmo tempo.

#Desenvolvimento

O bundle do number-flow só precisa ser regerado se você alterar resources/js/index.js:

npm install
npm run build

#Testes

composer test

#Veja também

gsferro/filament-stat-plus-easy — cards de stat com ícone no canto e borda de acento colorida, para Filament v3, v4 e v5. O StatPlus estende o OdometerStat deste pacote, então o contador animado vem junto — mais o skeleton de carregamento combinando.

#Changelog

Please see CHANGELOG for more information on what has changed recently.

#Contributing

Please see CONTRIBUTING for details.

#Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

#Credits

#License

The MIT License (MIT). Please see License File for more information.

Nouvelle version disponible.