Jarvis.WebPages 2.0.1

dotnet add package Jarvis.WebPages --version 2.0.1
                    
NuGet\Install-Package Jarvis.WebPages -Version 2.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Jarvis.WebPages" Version="2.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Jarvis.WebPages" Version="2.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Jarvis.WebPages" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Jarvis.WebPages --version 2.0.1
                    
#r "nuget: Jarvis.WebPages, 2.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Jarvis.WebPages@2.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Jarvis.WebPages&version=2.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Jarvis.WebPages&version=2.0.1
                    
Install as a Cake Tool

Jarvis.WebPages

Biblioteca de componentes TagHelper para construção de formulários em Razor Pages e MVC.

Instalação

Requer .NET 10.

dotnet add package Jarvis.WebPages

Registre os TagHelpers no _ViewImports.cshtml:

@addTagHelper *, Jarvis.WebPages

Adicione o script de máscaras e formatação e o CSS dos componentes no layout:

<link rel="stylesheet" href="/_content/Jarvis.WebPages/css/style.css" />
<script src="/_content/Jarvis.WebPages/js/init.js"></script>

Cores e tema

O CSS dos componentes não traz paleta própria: cada cor procura primeiro a variável do tema do app e, na falta dela, cai num neutro. Basta o app definir as suas para todos os componentes acompanharem.

:root {
    --c-primary: #0d6efd;
    --c-surface: #fff;
    --c-text: #212529;
    --c-text-muted: #6c757d;
    --c-border: #dee2e6;
    --c-border-soft: #f1f3f5;
    --radius: .375rem;
}
Variável do app Neutro Onde aparece
--c-primary #212529 Destaques: item selecionado, dia de hoje, contador, fab, atalhos
--c-surface #fff Fundo dos painéis, do calendário e das células
--c-text #212529 Texto dos componentes
--c-text-muted #6c757d Placeholder, dias da semana, texto secundário
--c-border #dee2e6 Bordas e linhas da grade do calendário
--c-border-soft #f1f3f5 Divisórias internas e dias fora do mês
--radius .375rem Raio de painéis e botões

Para mudar só um componente, redefina a variável --jarvis-* correspondente no elemento — é o que os atributos color do Fab, do Calendar e o asp-add-color dos selects fazem por dentro.

.minha-pagina .jarvis-calendar {
    --jarvis-primary: #e91e63;
}

Componentes

Propriedades Comuns (BaseTagHelper)

Propriedade Tipo Descrição
asp-for ModelExpression Binding com a propriedade do modelo
asp-readonly bool Torna o campo somente leitura
asp-disabled bool Desabilita o campo
asp-name string Sobrescreve o nome do campo

Todos os componentes geram automaticamente: label, input, texto descritivo (via [Display(Description)]) e mensagem de validação.

Como identificar o componente

As funções públicas de JavaScript — jarvisForm.set, jarvisForm.get, jarvisForm.clear, jarvisSelect.select, jarvisSelect.rebuild, jarvisCalendar.select, jarvisCalendar.clear e jarvisCalendar.rebuild — recebem o alvo no primeiro parâmetro e aceitam quatro formas:

Forma Exemplo
Elemento jarvisSelect.rebuild(document.querySelector('#cidades'), items)
Seletor CSS jarvisSelect.rebuild('#div-cidades [data-jarvis-multi-select]', items)
Id, sem o # jarvisCalendar.rebuild('agenda', data)
Nome do campo jarvisSelect.rebuild('CidadesDaUfIds', items)

O nome do campo é a propriedade do asp-for, com ou sem o prefixo do modelo — CidadesDaUfIds e Model.CidadesDaUfIds chegam no mesmo campo. Vale para o FormSelect e o FormMultiSelect, que gravam o nome em data-jarvis-select-name.

O alvo também não precisa ser o componente em si: pode ser um elemento acima (a div da coluna, o div que envolve o calendário) ou abaixo dele. A busca sobe pelo closest e, se não achar, desce pelo querySelector. Então isto tudo aponta para o mesmo campo:

jarvisSelect.rebuild('CidadesDaUfIds', items);
jarvisSelect.rebuild('#div-model-cidadesdaufids', items);
jarvisSelect.rebuild('#div-model-cidadesdaufids [data-jarvis-multi-select]', items);

Quando nada é encontrado, a função sai sem fazer nada — não lança erro.


Lendo e escrevendo os valores

jarvisForm altera qualquer campo da biblioteca pelo mesmo caminho da digitação: o valor é escrito, o componente formata, valida os limites e chama o asp-on-change (ou o asp-on-complete).

jarvisForm.set('Nome', 'Maria');
jarvisForm.set('Cpf', '45054532005');        // vira 450.545.320-05
jarvisForm.set('Valor', 1234.5);             // vira R$ 1.234,50
jarvisForm.set('DataNascimento', '1988-11-03');
jarvisForm.set('HoraInicio', '14:30');
jarvisForm.set('Agendamento', '2026-08-16T14:30');
jarvisForm.set('Uf', 'PE');
jarvisForm.set('CidadesIds', ['1', '5']);

jarvisForm.get('Valor');                     // "1234,50" (o mesmo que vai no post)
jarvisForm.get('DataNascimento');            // "1988-11-03"
jarvisForm.get('CidadesIds');                // ["1", "5"]

jarvisForm.clear('Uf');
Função Descrição
set Escreve o valor e reaplica a formatação. Devolve false se o campo não existe
get Devolve o valor lógico, não o texto exibido
clear Esvazia o campo, o mesmo que set(alvo, '')

O que entra e o que sai de cada componente:

Componente set aceita get devolve
FormText, FormTextArea, FormPassword texto texto
FormMask dígitos ou o valor já formatado texto com a máscara
FormNumeric número ou texto valor do hidden
FormDate yyyy-MM-dd ou dd/MM/yyyy yyyy-MM-dd
FormTime HH:mm HH:mm
FormDateTime yyyy-MM-ddTHH:mm yyyy-MM-ddTHH:mm
FormSelect valor da opção valor selecionado
FormMultiSelect valor ou array de valores array dos marcados
FormDropzone File ou array de File array de File
FormCheck, FormSwitch true, 1 ou o texto deles true ou false

Valores fora dos limites de asp-min/asp-max são recusados pelo componente, igual à digitação. No FormMultiSelect a lista informada substitui o que estava marcado. O alvo segue as formas de Como identificar o componente.


Validação

As anotações do modelo valem no cliente sem configuração nenhuma. Quando o jQuery Validate está na página, a biblioteca acerta sozinha os três pontos em que ele não daria conta:

  • Campos escondidos. O valor do FormSelect, do FormMultiSelect, do FormNumeric e do FormDropzone não fica no elemento que aparece na tela, e o validador ignora :hidden por padrão. A biblioteca tira esses campos da lista de ignorados e coloca a validação em quem leva o nome da propriedade, para a mensagem cair no lugar certo.
  • Formato de data e de número. As regras date e number do jQuery Validate só entendem o formato americano, e recusariam 16/08/2026 e 1234,50. Elas passam a aceitar também o formato brasileiro — a data com ou sem hora, o número com vírgula ou ponto.
  • Valor escrito por código. Escolher no painel, marcar uma opção ou chamar jarvisForm.set não dispara evento. A biblioteca emite um change e revê o campo, então o erro some assim que o valor é corrigido — o erro só é reavaliado se já estiver na tela.

A borda vermelha aparece no elemento visível de cada componente, mesmo quando a classe de erro fica no campo escondido.

Nada disso vaza para o resto da página. As regras trocadas desviam para a implementação original quando o campo não é de um componente da biblioteca, e a lista de ignorados do projeto é preservada — a biblioteca só acrescenta a exceção dos campos escondidos que guardam o valor. Um <input class="date"> de outra biblioteca continua validando como antes.

No FormMultiSelect, use [MinLength] em vez de [Required]. São dois motivos: no servidor uma coleção vazia não é nula, então o [Required] passaria; e no cliente o unobtrusive não aplica o required em checkbox, porque para ele checkbox marcado é o aceite de um bool. O [MinLength] vale nos dois lados, contando quantos itens estão marcados:

[MinLength(1, ErrorMessage = "Selecione ao menos uma cidade")]
public List<int> CidadesIds { get; set; } = [];

FormText

Campo de texto simples.

<form-text asp-for="Nome" asp-col-css="col-md-6" asp-placeholder="Digite o nome" />
Propriedade Tipo Padrão Descrição
asp-col-css string col-md-12 Classe CSS da coluna
asp-input-css string Classe CSS adicional no input
asp-placeholder string Placeholder do campo

FormTextArea

Campo de texto multilinha.

<form-text-area asp-for="Observacao" asp-col-css="col-md-12" asp-placeholder="Descreva..." />
Propriedade Tipo Padrão Descrição
asp-col-css string col-md-12 Classe CSS da coluna
asp-input-css string Classe CSS adicional no input
asp-placeholder string Placeholder do campo

FormPassword

Campo de senha.

<form-password asp-for="Senha" asp-col-css="col-md-6" />
Propriedade Tipo Padrão Descrição
asp-col-css string col-md-12 Classe CSS da coluna
asp-input-css string Classe CSS adicional no input

FormCheck

Caixa de marcação para uma propriedade bool. O texto ao lado sai do [Display(Name)].

<form-check asp-for="AceitoOsTermos" />
<form-check asp-for="Bloqueado" asp-col-css="col-md-6" />
Propriedade Tipo Padrão Descrição
asp-col-css string col-md-12 Classe CSS da coluna
asp-input-css string Classe CSS adicional no elemento clicável

O texto fica ao lado do controle, dentro do mesmo label, e não acima como nos demais campos — por isso o asp-input-css cai nesse label, e não no input, que é invisível.

Junto do campo vai um hidden com false, então a propriedade volta para false quando nada está marcado — sem ele o post não enviaria nada e o valor anterior permaneceria.

Com asp-disabled o campo não vai no post. Com asp-readonly ele não pode ser alterado, mas o valor continua sendo enviado: o readonly não vale para checkbox no navegador, então o componente desabilita a caixa e repete o valor num hidden.


FormSwitch

Mesmo comportamento do FormCheck, com a aparência de interruptor. As propriedades e as regras de asp-readonly e asp-disabled são as mesmas.

<form-switch asp-for="ComAnestesia" />
<form-switch asp-for="Ativo" asp-col-css="col-md-6" />

Use o FormSwitch quando o campo liga ou desliga um comportamento, e o FormCheck quando é um aceite ou um item de lista.


FormDate

Campo de data com máscara dd/mm/aaaa e calendário próprio da biblioteca — sem o picker nativo do navegador, que muda de cara em cada sistema. O valor é enviado como texto, então o model binding e a validação continuam iguais aos dos demais componentes.

<form-date asp-for="DataNascimento" asp-col-css="col-md-4" />
<form-date asp-for="DataNascimento" asp-col-css="col-md-4" asp-show-picker="false" />
Propriedade Tipo Padrão Descrição
asp-input-css string Classe CSS adicional no input
asp-col-css string col-md-12 Classe CSS da coluna
asp-min DateTime? Data mínima permitida
asp-max DateTime? Data máxima permitida
asp-on-change string Nome da função JS chamada ao alterar a data
asp-show-picker bool true Exibe o botão que abre o calendário

O botão ao lado do campo abre o calendário: setas para trocar de mês, o dia de hoje contornado e o dia escolhido preenchido. Datas fora de asp-min/asp-max ficam desabilitadas, e o painel abre para cima quando não há espaço abaixo. Com asp-show-picker="false" o campo fica só com a máscara.

Digitar também funciona: a máscara aceita apenas números, monta dd/mm/aaaa e descarta o que não forma uma data válida ou cai fora dos limites.

Exemplo com asp-on-change

<form-date asp-for="DataInicio" asp-min="DateTime.Today" asp-on-change="onDataInicioChange" />

<script>
    function onDataInicioChange(value) {
        // value = "2026-08-15" (formato ISO), ou "" quando o campo é limpo
    }
</script>

A função é chamada quando a data fica completa, tanto digitando quanto escolhendo no calendário.


FormTime

Campo de hora com máscara hh:mm e painel com as listas de hora e minuto. Aceita TimeSpan, TimeOnly e DateTime no modelo.

<form-time asp-for="HoraInicio" asp-col-css="col-md-3" />
Propriedade Tipo Padrão Descrição
asp-input-css string Classe CSS adicional no input
asp-col-css string col-md-12 Classe CSS da coluna
asp-min TimeSpan? Hora mínima permitida
asp-max TimeSpan? Hora máxima permitida
asp-minute-step int 5 Intervalo entre os minutos da lista
asp-on-change string Nome da função JS chamada ao alterar a hora
asp-show-picker bool true Exibe o botão que abre as listas
<form-time asp-for="HoraFim" asp-min="new TimeSpan(8, 0, 0)" asp-max="new TimeSpan(18, 0, 0)" asp-minute-step="15" />

Escolher a hora mantém o painel aberto; escolher o minuto fecha. O asp-on-change recebe "14:30", ou "" quando o campo é limpo.


FormDateTime

Campo de data e hora com máscara dd/mm/aaaa hh:mm. O painel traz o calendário e as listas de hora e minuto lado a lado.

<form-date-time asp-for="DataHora" asp-col-css="col-md-5" />
Propriedade Tipo Padrão Descrição
asp-input-css string Classe CSS adicional no input
asp-col-css string col-md-12 Classe CSS da coluna
asp-min DateTime? Data e hora mínimas permitidas
asp-max DateTime? Data e hora máximas permitidas
asp-minute-step int 5 Intervalo entre os minutos da lista
asp-on-change string Nome da função JS chamada ao alterar o valor
asp-show-picker bool true Exibe o botão que abre o painel
<form-date-time asp-for="Agendamento" asp-min="DateTime.Now" asp-minute-step="30" />

Escolher o dia preenche a hora com 00:00 — ou com o limite, quando 00:00 ficaria fora da faixa — e o painel só fecha ao escolher o minuto. O asp-on-change recebe "2026-08-15T14:30".


FormSelect

Campo de seleção com busca. Renderiza um painel próprio no lugar do <select> nativo, e o valor fica em um campo hidden — o model binding e a validação continuam iguais aos dos outros componentes.

<form-select asp-for="CidadeId"
             asp-items="Model.Cidades"
             asp-value-field="Id"
             asp-text-field="Nome"
             asp-item-field="Uf"
             asp-placeholder="Selecione a cidade"
             asp-on-change="onCidadeChange" />
Propriedade Tipo Padrão Descrição
asp-input-css string Classe CSS adicional no campo
asp-col-css string col-md-12 Classe CSS da coluna
asp-items IEnumerable Lista de itens para popular o campo
asp-on-change string Nome da função JS chamada ao alterar o valor
asp-text-field string Text Nome da propriedade usada como texto
asp-value-field string Value Nome da propriedade usada como value
asp-item-field string Nome da propriedade usada como texto secundário
asp-placeholder string Selecionar... Texto exibido quando nada está selecionado
asp-show-search bool true Exibe o campo de busca acima das opções
asp-search-placeholder string Buscar... Placeholder do campo de busca
asp-add-href string URL de um botão exibido ao lado do campo
asp-add-on-click string Nome da função JS chamada no clique do botão
asp-add-label string Adicionar Texto acessível do botão de atalho
asp-add-color string var(--c-primary, #212529) Cor da borda e do ícone do botão
asp-add-icon-color string #fff Cor do ícone do botão sob o ponteiro

O asp-item-field exibe um segundo texto abaixo do principal, útil para desambiguar itens de mesmo nome (cidade e UF, produto e código). A busca filtra pelo texto principal.

O asp-add-href renderiza um botão ao lado do campo, para cadastrar um item que ainda não existe:

<form-select asp-for="CidadeId" asp-items="Model.Cidades" asp-add-href="/cidades/novo" />

Com asp-add-href sai um <a>; só com asp-add-on-click, um <button type="button">, que chama a função sem navegar nem submeter o formulário. Os dois podem ser usados juntos.

<form-select asp-for="CidadeId" asp-items="Model.Cidades" asp-add-on-click="abrirModalCidade" />

O botão é quadrado, com a mesma altura do campo, e segue a mesma regra de cor do Fab: usa asp-add-color quando informado, senão a variável --c-primary do tema, e por último o neutro #212529. O asp-add-icon-color é a cor do ícone com o ponteiro sobre o botão.

Com asp-readonly ou asp-disabled o campo não abre. Lista vazia mostra "Nenhuma opção disponível".

Exemplo com asp-on-change

<form-select asp-for="EstadoId"
             asp-items="Model.Estados"
             asp-value-field="Id"
             asp-text-field="Nome"
             asp-on-change="onEstadoChange" />

<script>
    function onEstadoChange(value) {
        // value = id do estado selecionado
        console.log('Estado selecionado:', value);

        // Exemplo: carregar cidades do estado via AJAX
        fetch(`/api/cidades?estadoId=${value}`).
            then(r => r.json()).
            then(cidades => {
                // popular outro select de cidades
            });
    }
</script>

Selecionando pelo JavaScript

jarvisSelect.select marca um valor no campo, como se a opção tivesse sido clicada: grava o valor no campo hidden, escreve o texto, fecha o painel e chama o asp-on-change. Devolve false quando o valor não existe entre as opções.

<form-select asp-for="Uf" asp-items="Model.Estados" />
<form-mask asp-for="Cep" asp-type="CEP" asp-on-complete="onCepComplete" />

<script>
    function onCepComplete(value) {
        fetch(`https://viacep.com.br/ws/${value.replace(/\D/g, '')}/json/`).
            then(r => r.json()).
            then(endereco => jarvisSelect.select('Uf', endereco.uf));
    }
</script>

Passar '' ou null volta o campo ao placeholder. No FormMultiSelect a função aceita um valor ou um array, e a lista informada substitui o que estava marcado.

jarvisSelect.select('CidadesIds', ['1', '5']);

O alvo segue as formas de Como identificar o componente.


FormMultiSelect

Campo de seleção múltipla com busca. Cada item vira um checkbox com o mesmo name, então o model binding preenche a coleção indicada em asp-for.

<form-multi-select asp-for="CidadesIds"
                   asp-items="Model.Cidades"
                   asp-value-field="Id"
                   asp-text-field="Nome"
                   asp-item-field="Uf"
                   asp-placeholder="Selecione as cidades" />
Propriedade Tipo Padrão Descrição
asp-input-css string Classe CSS adicional no campo
asp-col-css string col-md-12 Classe CSS da coluna
asp-items IEnumerable Lista de itens para popular o campo
asp-on-change string Nome da função JS chamada ao marcar/desmarcar
asp-text-field string Text Nome da propriedade usada como texto
asp-value-field string Value Nome da propriedade usada como value
asp-item-field string Nome da propriedade usada como texto secundário
asp-placeholder string Selecionar... Texto exibido quando nada está selecionado
asp-show-search bool true Exibe o campo de busca acima das opções
asp-search-placeholder string Buscar... Placeholder do campo de busca
asp-show-select-all bool true Exibe a opção que marca e desmarca todos
asp-select-all-label string Selecionar todos Texto da opção que marca e desmarca todos
asp-add-href string URL de um botão exibido ao lado do campo
asp-add-on-click string Nome da função JS chamada no clique do botão
asp-add-label string Adicionar Texto acessível do botão de atalho
asp-add-color string var(--c-primary, #212529) Cor da borda e do ícone do botão
asp-add-icon-color string #fff Cor do ícone do botão sob o ponteiro

A propriedade do modelo precisa ser uma coleção (List<int>, List<Guid>, etc.). O campo exibe o texto do item quando há um selecionado e "N selecionados" a partir de dois.

Selecionar todos

A primeira opção do painel marca e desmarca os itens de uma vez, e reflete o estado atual: marcada com todos selecionados, indeterminada com parte deles. Ela não tem name, então não vai junto no post.

<form-multi-select asp-for="CidadesIds" asp-items="Model.Cidades" asp-select-all-label="Marcar todas" />
<form-multi-select asp-for="CidadesIds" asp-items="Model.Cidades" asp-show-select-all="false" />

Com uma busca em andamento, a opção age apenas sobre os itens visíveis — buscar "SP" e clicar em selecionar todos marca só o que o filtro deixou na tela, preservando o que já estava marcado fora dele.


Trocando as opções via AJAX

Os componentes usam delegação de eventos no document, então campos inseridos depois da carga (modal, partial via AJAX) já funcionam sem reinicializar nada.

Para trocar as opções de um campo que já está na tela, use jarvisSelect.rebuild, passando o campo e a lista nova. O jeito mais curto é usar o nome da propriedade do asp-for:

<form-select asp-for="IdConvenio" asp-items="Model.Convenios" asp-on-change="filtrarParticipantes" />
<form-multi-select asp-for="IdsParticipantes" asp-items="Model.Participantes" />

<script>
    function filtrarParticipantes(value) {
        fetch(`/agenda/agendar?handler=Participantes&idConvenio=${value}`, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }).
            then(r => r.json()).
            then(items => jarvisSelect.rebuild('IdsParticipantes', items));
    }
</script>

Veja Como identificar o componente para as outras formas de apontar o campo.

A lista aceita as chaves em PascalCase ou camelCase, então serializar um GenericList<T> direto do PageModel funciona:

public IActionResult OnGetParticipantes(Guid? idConvenio)
{
    return new JsonResult(_service.ListarParticipantes(idConvenio));
}
Item Descrição
value Valor enviado no formulário
text Texto principal
item Texto secundário (opcional)

O que estava selecionado é preservado quando o item continua existindo na lista nova, e descartado quando some — no FormSelect o campo volta ao placeholder. O campo de busca, a opção de selecionar todos e o name dos checkboxes são remontados conforme os atributos do componente, inclusive quando a lista tinha chegado vazia do servidor.

Quando o rebuild derruba o que estava selecionado, o asp-on-change é chamado com o valor novo ("" no FormSelect, a lista restante no FormMultiSelect), para os campos em cascata reagirem. Se a seleção sobrevive inteira, nada é disparado.

O asp-add-href renderiza um botão ao lado do campo, para cadastrar um item que ainda não existe:

<form-multi-select asp-for="CidadesIds" asp-items="Model.Cidades" asp-add-href="/cidades/novo" />
<form-multi-select asp-for="CidadesIds" asp-items="Model.Cidades" asp-add-on-click="abrirModalCidade" />

Com asp-add-href sai um <a>; só com asp-add-on-click, um <button type="button">, que chama a função sem navegar nem submeter o formulário. Os dois podem ser usados juntos.

O asp-on-change recebe a lista dos valores marcados:

<form-multi-select asp-for="CidadesIds" asp-items="Model.Cidades" asp-on-change="onCidadesChange" />

<script>
    function onCidadesChange(values) {
        // values = ["1", "5", "8"]
        console.log('Cidades selecionadas:', values);
    }
</script>

Nenhum item marcado não envia nada no post — a coleção chega vazia ou nula, conforme a inicialização da propriedade no modelo.


FormNumeric

Campo numérico com formatação automática (dinheiro, percentual, decimal, inteiro).

<form-numeric asp-for="Valor" asp-type="Money" asp-precision="2" asp-col-css="col-md-4" />
Propriedade Tipo Padrão Descrição
asp-col-css string col-md-12 Classe CSS da coluna
asp-input-css string Classe CSS adicional no input
asp-precision int 2 Casas decimais (limitado a 20)
asp-type NumericType Decimal Tipo de formatação
asp-allow-negative bool false Permite negativos; o sinal alterna a cada - digitado

NumericType

Valor Formatação
Money R$ 1.234,56
Percent 12,34%
Decimal 1.234,56
Integer 1.234

O valor digitado é limitado a 15 dígitos, teto a partir do qual o JavaScript perde precisão numérica.


FormMask

Campo com máscara de entrada (aceita apenas números, formatados automaticamente).

<form-mask asp-for="CPF" asp-type="CPF" asp-col-css="col-md-4" />
<form-mask asp-for="Codigo" asp-type="Custom" asp-mask="##.###-##" />
<form-mask asp-for="Quantidade" asp-type="OnlyNumbers" />
Propriedade Tipo Padrão Descrição
asp-col-css string col-md-12 Classe CSS da coluna
asp-input-css string Classe CSS adicional no input
asp-type MaskType CPF Tipo de máscara
asp-on-complete string Nome da função JS chamada ao completar a máscara
asp-mask string Máscara personalizada (usado com Custom)

MaskType

Valor Máscara Exemplo
CPF ###.###.###-## 123.456.789-00
CNPJ ##.###.###/####-## 12.345.678/0001-90
CPF_CNPJ Automático por tamanho CPF ou CNPJ
CEP #####-### 12345-678
PhoneNumber (##) #####-#### (11) 99876-5432
OnlyNumbers Sem máscara, só dígitos 123456
Custom Definida por asp-mask Conforme o pattern

Exemplo com asp-on-complete

O callback é disparado quando o usuário termina de preencher todos os dígitos da máscara.

<form-mask asp-for="CPF"
           asp-type="CPF"
           asp-col-css="col-md-4"
           asp-on-complete="onCpfComplete" />

<script>
    function onCpfComplete(value) {
        // value = "123.456.789-00" (com máscara)
        console.log('CPF preenchido:', value);

        // Exemplo: buscar dados do cliente pelo CPF
        const cpf = value.replace(/\D/g, '');
        fetch(`/api/clientes?cpf=${cpf}`).
            then(r => r.json()).
            then(cliente => {
                // preencher campos do formulário
            });
    }
</script>
<form-mask asp-for="CEP"
           asp-type="CEP"
           asp-col-css="col-md-3"
           asp-on-complete="onCepComplete" />

<script>
    function onCepComplete(value) {
        // value = "12345-678"
        const cep = value.replace(/\D/g, '');
        fetch(`https://viacep.com.br/ws/${cep}/json/`).
            then(r => r.json()).
            then(endereco => {
                document.querySelector('[name="Logradouro"]').value = endereco.logradouro;
                document.querySelector('[name="Bairro"]').value = endereco.bairro;
                document.querySelector('[name="Cidade"]').value = endereco.localidade;
            });
    }
</script>

Máscara Custom

Use # para representar um dígito numérico. Qualquer outro caractere é inserido automaticamente como separador fixo. O asp-on-complete também funciona com máscaras customizadas.

<form-mask asp-for="Protocolo"
           asp-type="Custom"
           asp-mask="####/####-##"
           asp-on-complete="onProtocoloComplete" />

<script>
    function onProtocoloComplete(value) {
        // value = "2024/0001-01"
        console.log('Protocolo preenchido:', value);
    }
</script>

Exemplos de patterns:

##.###-##    →  12.345-67
####/####    →  2024/0001
###-##       →  123-45
####/####-## →  2024/0001-01

FormDropzone

Área de envio de arquivos: aceita arrastar e soltar ou clicar para escolher, e lista o que foi selecionado com o tamanho e um botão para remover. O campo é um <input type="file"> comum, então o model binding continua igual ao dos demais componentes.

<form method="post" enctype="multipart/form-data">
    <form-dropzone asp-for="Anexo" asp-col-css="col-md-6" />
    <form-dropzone asp-for="Anexos" asp-col-css="col-md-6" asp-multiple="true" />
</form>
[BindProperty] public IFormFile Anexo { get; set; }
[BindProperty] public List<IFormFile> Anexos { get; set; } = [];
Propriedade Tipo Padrão Descrição
asp-col-css string col-md-12 Classe CSS da coluna
asp-input-css string Classe CSS adicional na área
asp-multiple bool false Permite escolher vários arquivos
asp-accept string Tipos aceitos, igual ao accept do HTML
asp-max-size int Tamanho máximo de cada arquivo, em MB
asp-title string Arraste o arquivo aqui Texto principal da área
asp-subtitle string ou clique para escolher Texto secundário da área
asp-remove-label string Remover Texto acessível do botão que remove um arquivo
asp-camera bool false Faixa dentro da área que abre a câmera
asp-camera-label string Tirar foto Texto da faixa da câmera
asp-camera-facing string environment Câmera aberta: environment ou user
asp-color string var(--c-primary, …) Cor de destaque do campo
asp-on-change string Nome da função JS chamada quando a lista muda

O formulário precisa de enctype="multipart/form-data". Sem asp-title, o texto acompanha o asp-multiple (Arraste os arquivos aqui no plural).

O asp-color redefine a cor de destaque dentro do próprio campo, igual ao color do Fab e do Calendar: vale para o ícone, a borda sob o ponteiro, o destaque durante o arrasto e o ícone de cada arquivo da lista.

<form-dropzone asp-for="Anexos" asp-multiple="true" asp-color="#e91e63" />

Um ou vários arquivos

Com asp-multiple, os arquivos soltos vão se somando aos que já estavam na lista, sem repetir nome e tamanho iguais — arrastar em duas levas funciona. Sem ele, o novo arquivo substitui o anterior, e soltar vários de uma vez guarda só o primeiro.

Escolher pela janela do sistema segue o comportamento nativo: a seleção nova substitui a anterior.

Tipos e tamanho

<form-dropzone asp-for="Imagens" asp-multiple="true" asp-accept="image/*" asp-max-size="1" />

O asp-accept vale tanto no clique (o navegador filtra a janela) quanto no arrastar, onde a biblioteca confere a extensão e o tipo. Arquivos recusados não entram na lista e aparecem num aviso abaixo da área, separado por motivo: Tipo não aceito: planilha.xlsx • Acima de 1 MB: foto.png. Quando o sistema não informa o tipo do arquivo, a extensão decide.

A checagem é do navegador, para poupar o envio: valide também no servidor.

Câmera

<form-dropzone asp-for="Anexos" asp-multiple="true" asp-camera />
<form-dropzone asp-for="Foto" asp-camera asp-camera-facing="user" asp-camera-label="Tirar selfie" />

Com asp-camera, a própria área ganha embaixo uma faixa separada por uma linha: em cima continua o arrastar e o clique de sempre, e embaixo o atalho que abre a câmera do aparelho direto, sem passar pela galeria. É a mesma caixa, não um segundo campo.

A foto entra pelo mesmo caminho do arrastar: com asp-multiple ela soma à lista, e sem ele substitui o arquivo atual. O asp-accept e o asp-max-size continuam valendo — e se o asp-accept não deixar passar imagem, a faixa nem aparece, em vez de aceitar a foto para recusar logo depois.

No computador não existe câmera para o navegador abrir, então a faixa cai na janela de arquivos — o recurso é feito para tela mobile.

Callback

<form-dropzone asp-for="Anexos" asp-multiple="true" asp-on-change="onArquivos" />

<script>
    function onArquivos(files) {
        // files = [{ name: "contrato.pdf", size: 3072 }]
        document.getElementById('enviar').hidden = files.length === 0;
    }
</script>

A função é chamada a cada mudança na lista, seja ao adicionar ou ao remover, e recebe um array com o nome e o tamanho de cada arquivo. Útil para só exibir o botão de envio quando houver algo.

Com asp-readonly ou asp-disabled a área fica apagada, sem clique e sem aceitar arrastar.


Alerts

Componente de alertas Bootstrap que exibe mensagens armazenadas via TempData. Renderiza alertas de info (azul), sucesso (verde), aviso (amarelo) e erro (vermelho).

Na view (layout ou página):

<alerts></alerts>

No PageModel:

// Alerta informativo (azul)
this.Info("Operação realizada.");

// Alerta de sucesso (verde)
this.Success("Registro salvo com sucesso!");

// Alerta de aviso (amarelo)
this.Warning("Atenção: campo opcional não preenchido.");

// Alerta de erro (vermelho)
this.Error("Falha ao salvar o registro.");

return RedirectToPage();

As mensagens são exibidas automaticamente na próxima renderização da página e descartadas após a exibição.

Cada alerta traz um ícone em SVG da própria biblioteca (jarvis-alert-icon), sem depender de fonte de ícones: fa-circle-info na informação, fa-circle-check no sucesso, fa-triangle-exclamation no aviso e fa-circle-exclamation no erro.

Migrando dos ícones do Bootstrap Icons. Até a versão anterior o alerta usava <i class="bi bi-...">, que só aparecia com o Bootstrap Icons carregado. Se o app.css escondia esse ícone para desenhar outro, apague as regras:

.alert .bi {
    display: none;
}

.alert::before {
    /* ícone próprio */
}

Senão o alerta fica com dois ícones. Para mudar o tamanho ou o espaço do ícone, sobrescreva .jarvis-alert-icon.

Fora do PageModel (filtros e middlewares)

Onde não há PageModel (filtro de autorização, middleware), use as mesmas extensões sobre o HttpContext. Elas gravam no TempData e já chamam Save():

filterContext.HttpContext.Info("Operação realizada.");
filterContext.HttpContext.Success("Registro salvo com sucesso!");
filterContext.HttpContext.Warning("Atenção: assinatura vence em 3 dias.");
filterContext.HttpContext.Error(minhaConta.Message);

O Save() é necessário porque o filtro de autorização roda antes do SaveTempDataFilter do ASP.NET. Ao interromper a requisição com filterContext.Result, o TempData não é salvo sozinho e a mensagem se perde.

Migrando código existente. Antes:

public void OnAuthorization(AuthorizationFilterContext filterContext)
{
    // ...

    var tempData = filterContext.HttpContext.RequestServices.GetRequiredService<ITempDataDictionaryFactory>().GetTempData(filterContext.HttpContext);

    tempData["ErrorMessage"] = minhaConta.Message;

    tempData.Save();

    filterContext.Result = new RedirectToPageResult("/MinhaConta");
}

Depois:

public void OnAuthorization(AuthorizationFilterContext filterContext)
{
    // ...

    filterContext.HttpContext.Error(minhaConta.Message);

    filterContext.Result = new RedirectToPageResult("/MinhaConta");
}

As chaves (InfoMessage, SuccessMessage, WarningMessage, ErrorMessage) são as mesmas, então o componente <alerts> continua funcionando sem mudança. Os using de ITempDataDictionaryFactory e GetRequiredService podem ser removidos se não forem mais usados.

Não chame as extensões do HttpContext depois que a resposta começou a ser enviada: o cookie do TempData não é gravado.


Helpers

is-visible

Controla a visibilidade de qualquer elemento HTML. Quando false, o elemento é completamente removido do output.

<div is-visible="Model.ExibirDetalhes">
    Conteúdo visível apenas quando ExibirDetalhes for true.
</div>

<button is-visible="Model.PodeExcluir">Excluir</button>

disabled

Adiciona o atributo disabled a qualquer elemento HTML quando a condição é true.

<button disabled="Model.Processando">Salvar</button>
<input disabled="!Model.PodeEditar" />

Pagination

Navegação entre páginas de uma listagem, no formato ‹ 1 … 4 5 6 … 20 ›. Os links preservam a query string atual e trocam apenas o parâmetro da página, então filtros e ordenação sobrevivem à navegação. Com uma página só, nada é renderizado.

<pagination page-number="Model.PageNumber" total-pages="Model.TotalPages" />
Propriedade Tipo Padrão Descrição
page-number int 1 Página atual, começando em 1
total-pages int Total de páginas
query-key string pageNumber Nome do parâmetro da página na query string
sibling-count int 1 Páginas exibidas de cada lado da atual
label string Paginação Texto acessível da navegação
page-label string Página {0} Texto acessível de cada número ({0} = página)
previous-label string Página anterior Texto acessível do botão anterior
next-label string Próxima página Texto acessível do próximo botão

A primeira e a última página aparecem sempre, e o sibling-count define quantas ficam de cada lado da atual — o salto entre um número e outro vira reticências. Na página 10 de 20, o padrão mostra 1 … 9 10 11 … 20; com sibling-count="3", 1 … 7 8 9 10 11 12 13 … 20.

A página atual é um <span> destacado na cor primária, sem link. Nas pontas, a seta continua no lugar, apagada e sem clique, em vez de sumir e fazer a barra pular de posição.

Abaixo de 576px a lista de números não cabe, então ficam só as setas e a página atual — ‹ 6 ›.

Cuidado com query-key="page": em Razor Pages, page é reservado pela rota. O model binding tenta converter o caminho da página para número e o handler recebe zero. Por isso o padrão é pageNumber.

public void OnGet(int pageNumber)
{
    PageNumber = pageNumber > 0 ? pageNumber : 1;
}

Calendar

Calendário mensal com a contagem de itens por dia. Com href, as setas navegam para {href}?{query-key}=yyyy-MM-dd; sem ele, viram botões que chamam o JavaScript de on-month-change. Nos dois casos o mês vai como o primeiro dia dele, em formato ISO. O clique em qualquer dia chama o JavaScript de on-day-select.

<calendar year="Model.Year" month="Model.Month" href="/agenda" days="Model.Days" />
Propriedade Tipo Padrão Descrição
year int ano atual Ano exibido
month int mês atual Mês exibido, de 1 a 12
days IDictionary<int, int> Quantidade de itens por dia do mês
color string var(--c-primary, #212529) Cor de destaque do calendário
href string URL base da navegação entre meses
query-key string month Nome do parâmetro do mês na query string
on-month-change string Nome da função JS chamada ao trocar de mês
on-day-select string Nome da função JS chamada ao escolher um dia
previous-label string Mês anterior Texto acessível do botão do mês anterior
next-label string Próximo mês Texto acessível do botão do próximo mês
picker-label string Escolher mês e ano Texto acessível do cabeçalho

O days recebe o dia do mês e a quantidade de itens. Os dias presentes no dicionário ganham destaque e mostram o contador, mas o clique vale para qualquer dia do mês.

public Dictionary<int, int> Days { get; set; } = new() { { 3, 1 }, { 8, 4 }, { 15, 2 } };

O on-day-select recebe a data em formato ISO de qualquer dia clicado, tenha itens ou não, e o componente marca o dia escolhido:

<calendar year="2026" month="8" href="/agenda" days="Model.Days" on-day-select="mostrarDia" />

<script>
    function mostrarDia(date) {
        // date = "2026-08-15"
        console.log('Dia selecionado:', date);
    }
</script>

Os nomes dos dias da semana e do mês vêm da cultura da requisição. O mês fica à esquerda, com o ano abaixo em texto menor, e os botões de navegação à direita. O dia de hoje aparece destacado.

Botões extras no cabeçalho

O conteúdo da tag é renderizado depois das setas, para a página colocar os próprios botões:

<calendar year="Model.Year" month="Model.Month" days="Model.Days" on-month-change="carregarMes">
    <button type="button" class="btn btn-sm btn-secondary" onclick="irParaHoje()">Hoje</button>
</calendar>

Trocando o mês sem recarregar

Sem href, as setas viram <button type="button"> e chamam o on-month-change com o primeiro dia do mês em formato ISO — o mesmo formato do on-day-select. A página busca os dados e troca o calendário; como o JavaScript usa delegação no document, o calendário novo já funciona sem reinicializar nada.

<div id="agenda">
    <calendar year="Model.Year" month="Model.Month" days="Model.Days" on-month-change="carregarMes" on-day-select="mostrarDia" />
</div>

<script>
    function carregarMes(date) {
        // date = "2026-07-01"
        fetch(`/agenda?handler=Calendario&month=${date}`, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }).
            then(r => r.text()).
            then(html => document.getElementById('agenda').innerHTML = html);
    }
</script>

Com os dois atributos juntos, a seta navega e ainda chama a função.

Trocando o mês por JSON

Em vez do HTML inteiro, o servidor pode devolver só os dados e o jarvisCalendar.rebuild remonta o cabeçalho e a grade — mesma ideia do jarvisSelect.rebuild. O componente devolvido pelo servidor continua no lugar: label, cor, callbacks e o seletor de mês seguem valendo.

O modelo é o CalendarModel:

public JsonResult OnGetMes(DateTime month)
{
    var days = ObterItensPorDia(month);

    return new JsonResult(CalendarModel.Create(month, days));
}

O Create aceita a data do mês ou o par ano e mês, e já preenche o MonthName na cultura da requisição:

CalendarModel.Create(new DateTime(2026, 5, 1), days);
CalendarModel.Create(2026, 5, days);
CalendarModel.Create(2026, 5);
{ "year": 2026, "month": 5, "monthName": "Maio", "days": { "3": 1, "8": 4, "15": 2 } }
<div id="agenda">
    <calendar year="Model.Year" month="Model.Month" days="Model.Days" on-month-change="carregarMes" on-day-select="mostrarDia" />
</div>

<script>
    function carregarMes(date) {
        fetch(`/agenda?handler=Mes&month=${date}`).
            then(r => r.json()).
            then(data => jarvisCalendar.rebuild('agenda', data));
    }
</script>
Campo Obrigatório Descrição
year sim Ano exibido
month sim Mês exibido, de 1 a 12
monthName não Texto do cabeçalho; sem ele o nome sai da cultura da página
days não Contagem por dia; sem ele o mês fica sem destaques e sem contador

As chaves valem em camelCase ou PascalCase, e days aceita tanto o objeto ({ "3": 1 }) quanto a lista ([{ "day": 3, "count": 1 }]). O alvo segue as formas de Como identificar o componente — no exemplo, o id da div que envolve o calendário.

O que o rebuild faz, na ordem:

  1. Escreve o mês e o ano no cabeçalho.
  2. Remonta a grade, incluindo as células vazias que fecham a primeira e a última semana.
  3. Marca o dia de hoje quando ele cai no mês exibido.
  4. Repõe o destaque e o contador dos dias que vieram em days.
  5. Reaponta as setas para os meses vizinhos, no href ou na chamada do on-month-change.
  6. Atualiza o mês do seletor de mês e ano, e fecha o painel se estava aberto.
  7. Mantém o dia marcado, se a data continuar existindo no mês novo.

O que não muda: a cor (color), os callbacks, os botões extras do cabeçalho e os textos acessíveis — tudo isso continua vindo do componente renderizado pelo servidor.

A partir de 768px a grade ganha linhas separando os dias. No mobile as células ficam soltas, com cantos arredondados e sem linhas, para não pesar em tela pequena.

Escolhendo mês e ano

O cabeçalho é um botão: clicar nele abre um painel com os doze meses e setas para trocar de ano. Escolher um mês segue o mesmo caminho das setas — navega quando há href, ou chama o on-month-change com a data ISO do primeiro dia.

O painel é montado no cliente, então dá para percorrer vários anos sem recarregar a página. Ele fecha ao escolher um mês, ao clicar num dia ou ao clicar fora, e sempre reabre no ano do mês exibido. Os nomes dos meses vêm da cultura da requisição.

Sem href e sem on-month-change não existe para onde navegar: o cabeçalho fica como texto, sem setas e sem o seletor.

Cor

O color redefine a variável de destaque dentro do próprio calendário, então vale só para ele: dias com itens, dia de hoje, dia selecionado, contador e botões de navegação seguem a cor.

<calendar year="2026" month="8" days="Model.Days" color="#e91e63" />

Selecionando pelo JavaScript

jarvisCalendar.select marca um dia e chama o on-day-select, como se ele tivesse sido clicado. Aceita o número do dia ou a data em formato ISO, e devolve false quando o dia não existe no mês exibido.

<div id="agenda">
    <calendar year="2026" month="8" href="/agenda" days="Model.Days" on-day-select="mostrarDia" />
</div>

<script>
    jarvisCalendar.select('agenda', 15);
    jarvisCalendar.select('agenda', '2026-08-15');

    jarvisCalendar.clear('agenda');
</script>

Útil para restaurar o dia escolhido depois de recarregar a página ou trocar de mês. O clear apenas desmarca, sem chamar o on-day-select.


Sheet

Janela modal montada a partir do próprio formulário: o form vira o painel da janela, e o componente acrescenta o cabeçalho, o corpo em volta do conteúdo e o rodapé. Abaixo de 576px ela sobe do rodapé como uma folha, com alça para arrastar e fechar; a partir daí fica centralizada, como um modal comum.

Este é o único componente da biblioteca que precisa do JavaScript do Bootstrap, que cuida da abertura, do fundo escurecido, do foco e do fechamento pelo Esc. Sem ele a janela não chega a abrir, e nada quebra no resto da página.

<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalComentario">Comentar</button>

<form method="post" sheet="modalComentario" sheet-title="Novo comentário" sheet-action="Enviar">
    <div class="row g-3">
        <form-text-area asp-for="Comentario" asp-placeholder="Escreva seu comentário..." />
    </div>
</form>
Propriedade Tipo Padrão Descrição
sheet string Id da janela, usado no data-bs-target de quem abre
sheet-title string Título exibido no cabeçalho
sheet-action string Texto do botão que envia. Sem ele, não há rodapé
sheet-cancel string Cancelar Texto do botão que fecha sem enviar
sheet-style string primary Variação do botão de envio, no padrão Bootstrap

O form continua sendo um formulário comum: method, action, asp-page-handler e os campos dentro dele funcionam como em qualquer outro lugar. Sem sheet-action a janela sai sem rodapé, para conteúdo apenas de leitura.

No celular, arrastar o cabeçalho para baixo mais de 110px fecha a janela; menos que isso ela volta para o lugar. O arrasto é ignorado no desktop.


Fab

Botão de ação flutuante fixo no canto inferior direito, visível apenas no mobile — fica oculto a partir de 768px. Renderiza um link circular com ícone Font Awesome.

<fab href="/clientes/novo" icon="fa-solid fa-plus" label="Novo cliente" />
<fab href="/pedidos/novo" icon="bi bi-file-earmark-text" color="#e91e63" icon-color="#fff" label="Novo pedido" />
Propriedade Tipo Padrão Descrição
href string URL de destino ao clicar
on-click string Nome da função JS chamada no clique
icon string + em SVG Classe do ícone (em <i>) ou SVG
color string var(--c-primary, #212529) Cor de fundo do botão
icon-color string #fff Cor do ícone
label string Texto acessível do botão (aria-label)
class string Classes extras, somadas à jarvis-fab

Com href sai um <a>; sem ele, um <button type="button">, para o clique apenas chamar a função sem navegar nem submeter o formulário em volta. Os dois podem ser usados juntos.

<fab on-click="abrirFiltro" icon="fa-solid fa-filter" label="Filtrar" />

O icon é repassado como veio, sem prefixo: serve para Font Awesome, Bootstrap Icons, Material Icons ou qualquer outra biblioteca. A biblioteca escolhida precisa estar carregada na página. O icon também aceita um SVG em texto (começando com <), que sai como veio. Sem icon e sem conteúdo na tag, o botão usa um "+" desenhado em SVG, que não depende de biblioteca alguma.

Para um ícone que não venha de fonte, informe o conteúdo dentro da própria tag — ele substitui o <i>:

<fab href="/carrinho" label="Carrinho">
    <svg viewBox="0 0 24 24"><path d="..." /></svg>
</fab>

svg e img dentro do botão são dimensionados em 1.5em (30px) e herdam a cor do ícone via fill: currentColor.

A cor pode vir do atributo color ou do tema do app, pela variável CSS --c-primary. Sem nenhum dos dois, o fallback é #212529.


Topbar

Barra superior fixa no topo da página, com botão de voltar, título e botão de início. Os ícones são SVG, sem depender de biblioteca de ícones.

<topbar title="Minha conta" />
<topbar title="Editar cliente" back="/clientes" />
<topbar title="Relatório" back="/relatorios" home="" />
Propriedade Tipo Padrão Descrição
title string Título exibido na barra
back string / URL do botão de voltar. Vazio esconde o botão
home string / URL do botão de início. Vazio esconde o botão
color string var(--brand-grad) ou var(--c-primary) Cor de fundo da barra
radius int 24 Arredondamento dos cantos de baixo, em pixels
<topbar title="Sem arredondamento" radius="0" />

O conteúdo interno (.jarvis-topbar-inner) tem largura máxima de 760px, alinhado ao .app-content das páginas.

Ações

O conteúdo da tag vira a área de ações, entre o título e o botão de início. Use <topbar-action> para botões com ícone:

<topbar title="Pedidos" back="/">
    <topbar-action href="/pedidos/novo" icon="fa-solid fa-plus" label="Novo pedido" />
    <topbar-action on-click="abrirFiltro" icon="fa-solid fa-filter" label="Filtrar" badge="3" />
</topbar>
Propriedade Tipo Padrão Descrição
href string URL de destino ao clicar
on-click string Nome da função JS chamada no clique
icon string Classe do ícone (em <i>) ou SVG
label string Texto do title e do aria-label
badge int 0 Contador sobre o ícone. Acima de 99 mostra 99+
class string Classes extras, somadas à jarvis-topbar-action

Com href sai um <a>; sem ele, um <button type="button">. O <topbar-action> só é reconhecido dentro de <topbar>.

Para mostrar a ação só no desktop, use as classes do Bootstrap:

<topbar-action href="/pedidos/novo" icon="fa-solid fa-plus" label="Novo pedido" class="d-none d-md-inline-flex" />

Para um ícone que não venha de fonte, informe o SVG dentro da tag:

<topbar-action href="/carrinho" label="Carrinho">
    <svg viewBox="0 0 24 24"><path d="..." /></svg>
</topbar-action>

A área de ações também aceita HTML livre, como um dropdown do Bootstrap:

<topbar title="Início" back="" home="">
    <div class="dropdown">
        <button type="button" class="jarvis-topbar-action" data-bs-toggle="dropdown" aria-expanded="false" title="Conta">P</button>
        <ul class="dropdown-menu dropdown-menu-end">
            <li><a class="dropdown-item" href="/auth/minha-conta">Minha conta</a></li>
            <li><a class="dropdown-item text-danger" href="/auth/logout">Sair</a></li>
        </ul>
    </div>
</topbar>

Migrando do partial. Antes:

<partial name="_Topbar" model='("Minha conta", "/")' />

Depois:

<topbar title="Minha conta" back="/" />

Apague Pages/Shared/_Topbar.cshtml e as regras .topbar, .topbar-inner, .topbar-back, .topbar-title e .topbar-home do app.css.


AppFooter

Rodapé com o ano atual e o link da empresa. Por padrão fica oculto abaixo de 768px; use hide-on-mobile="false" para exibir também no mobile.

<app-footer />
<app-footer company="Minha Empresa" url="https://minhaempresa.com.br/" />
Propriedade Tipo Padrão Descrição
company string SoftPi Tecnologia Nome da empresa no link
url string https://softpitecnologia.com.br/ Site aberto em nova aba
hide-on-mobile bool true Esconde abaixo de 768px

Usa margin-top: auto para ficar no fim da página quando o body é display: flex; flex-direction: column.

Migrando do partial. Antes:

<partial name="_Footer" />

Depois:

<app-footer />

Apague Pages/Shared/_Footer.cshtml e as regras .app-footer do app.css.


Tabbar

Barra de abas presa no fim da tela, com ícone, texto e contador em cada item.

<tabbar>
    <tabbar-item href="/" icon="fa-solid fa-house" text="Início" />
    <tabbar-item href="/pedidos" icon="fa-solid fa-receipt" text="Pedidos" badge="2" />
    <tabbar-item on-click="abrirMenu" icon="fa-solid fa-bars" text="Menu" />
</tabbar>
Propriedade Tipo Padrão Descrição
color string var(--c-primary, #212529) Cor do item ativo
hide-on-desktop bool false Esconde a partir de 768px

<tabbar-item>

Propriedade Tipo Padrão Descrição
href string URL de destino ao clicar
on-click string Nome da função JS chamada no clique
icon string Classe do ícone (em <i>) ou SVG
text string Texto abaixo do ícone
badge int 0 Contador sobre o ícone. Acima de 99 mostra 99+
active bool? automático Força o item como ativo ou inativo
class string Classes extras, somadas à jarvis-tabbar-item

Com href sai um <a>; sem ele, um <button type="button">. O <tabbar-item> só é reconhecido dentro de <tabbar>, e um SVG dentro da tag substitui o icon.

Item ativo. Sem active, o item fica ativo quando a URL atual é igual ao href ou está abaixo dele: href="/pedidos" fica ativo em /pedidos e em /pedidos/123. O href="/" só fica ativo na raiz. Quando a regra não servir, force com active="true" ou active="false".

A barra usa position: sticky e margin-top: auto, então deve ser o último elemento do body com display: flex; flex-direction: column. Respeita a área segura do iPhone (safe-area-inset-bottom).


BottomAction

Card flutuante preso no fim da tela, com o botão da ação principal da página. Aparece no mobile e no desktop, com a mesma largura do conteúdo (760px).

<bottom-action href="/clientes/novo" text="Novo cliente" />
<bottom-action on-click="abrirFiltro" icon="fa-solid fa-filter" text="Filtrar" />
Propriedade Tipo Padrão Descrição
href string URL de destino ao clicar
on-click string Nome da função JS chamada no clique
text string Texto do botão
icon string Classe do ícone (em <i>) ou SVG
color string var(--c-primary, #212529) Cor de fundo do botão
text-color string #fff Cor do texto e do ícone
radius int --radius + padding Arredondamento do card, em pixels
padding int 12 Espaço interno do card, em pixels
spacing int 16 Distância do card até as bordas da tela
gap int 8 Espaço entre os botões dentro do card
class string Classes extras do card

Com href sai um <a>; sem ele, um <button type="button">, para o clique apenas chamar a função sem navegar.

Para um ícone que não venha de fonte, informe o SVG no próprio icon:

<bottom-action href="/carrinho" text="Carrinho" icon="<svg viewBox='0 0 24 24'><path d='...' /></svg>" />

svg e img dentro do botão são dimensionados em 1.2em e herdam a cor do texto via fill: currentColor.

Para mais de um botão, informe o conteúdo dentro da tag. Ele substitui o botão, e cada filho direto ocupa a mesma largura:

<bottom-action>
    <a href="/pedidos/novo" class="jarvis-bottom-action-button">Novo pedido</a>
    <button type="button" class="jarvis-bottom-action-button" onclick="abrirFiltro()">Filtrar</button>
</bottom-action>

Usa position: sticky e margin-top: auto, então deve ficar depois do conteúdo e antes do <app-footer>, no body com display: flex; flex-direction: column. Por ser sticky, ocupa o próprio espaço no fim da página e não cobre o último item da lista. Respeita a área segura do iPhone (safe-area-inset-bottom).

Migrando do Fab. Antes:

<fab href="/clientes/novo" label="Novo cliente" />

Depois:

<bottom-action href="/clientes/novo" text="Novo cliente" />

Campo de busca em um formulário GET, com lupa e botão de enviar. O valor digitado volta preenchido, lido da query string.

<search-bar />
<search-bar name="termo" placeholder="Buscar clientes" />
<search-bar placeholder="Buscar pedidos" hide-button="true" class="mb-3" />
Propriedade Tipo Padrão Descrição
name string search Nome do campo na query string
placeholder string Buscar Texto de ajuda, também no aria-label
hide-button bool false Esconde o botão de enviar
color string var(--c-primary, #212529) Cor de fundo do botão de enviar
class string Classes extras do formulário
target string Id da lista filtrada no navegador

Sem o botão, a busca é enviada pelo Enter. O formulário envia só o campo da busca: outros parâmetros da URL (página, filtros) não são mantidos. A lupa e a seta são SVG do IconHelper. O campo usa .form-control, então segue a altura, o padding, a fonte, a borda e o foco dos demais inputs do projeto; o botão acompanha a altura do campo.

Filtro local

Com target, o campo filtra no navegador os itens de uma lista da página, sem formulário, sem botão e sem ir ao servidor. A comparação ignora maiúsculas e acentos: "sao" acha "São Paulo".

<search-bar target="listaClientes" placeholder="Filtrar clientes" />

<div id="listaClientes">
    <div class="card" data-search="Maria Silva maria@email.com">...</div>
    <div class="card" data-search="João Souza joao@email.com">...</div>
</div>

<div class="empty-state" data-search-empty="listaClientes" hidden>Nenhum cliente encontrado.</div>
  • O texto comparado vem do data-search de cada item, que pode estar em qualquer nível dentro da lista. Sem nenhum item com data-search, são filtrados os filhos diretos da lista pelo texto visível.
  • Os itens fora do filtro recebem hidden. Se o CSS do item define display, garanta [hidden] { display: none !important; } (o Bootstrap já traz).
  • Elementos com data-search-empty="{id da lista}" aparecem quando nada é encontrado.
  • name, hide-button e color não se aplicam, e o Enter não envia o formulário em volta.

Migrando de uma busca própria

Projetos que já têm um campo de busca feito à mão (tag helper, partial ou HTML direto na view) trocam pelo componente.

Busca enviada ao servidor. Antes:

<form method="get" class="minha-busca">
    <i class="fa-solid fa-magnifying-glass"></i>
    <input type="text" name="search" value="@Request.Query["search"]" placeholder="Pesquisar..." />
    <button type="submit"><i class="fa-solid fa-arrow-right"></i></button>
</form>

Depois:

<search-bar placeholder="Pesquisar..." />

Se o campo não se chama search, informe em name. O valor da query string volta preenchido sozinho.

Filtro local. Antes:

<div class="minha-busca">
    <i class="fa-solid fa-magnifying-glass"></i>
    <input type="text" oninput="filtrarLista(this, 'listaItens')" placeholder="Pesquisar..." />
</div>

<div id="listaItens">
    <div data-nome="Item um">...</div>
</div>

<div data-search-empty="listaItens" hidden>Nada encontrado.</div>

Depois:

<search-bar target="listaItens" placeholder="Pesquisar..." />

<div id="listaItens">
    <div data-search="Item um">...</div>
</div>

<div data-search-empty="listaItens" hidden>Nada encontrado.</div>

Equivalências:

Busca própria Componente
Classe do wrapper jarvis-search-bar (e jarvis-search-bar-no-button sem botão)
Ícone de lupa em <i> jarvis-search-bar-icon, com SVG da biblioteca
Botão de enviar jarvis-search-bar-button, com SVG da biblioteca
Atributo com o texto do item (ex: data-nome) data-search
Elemento de lista vazia data-search-empty="{id da lista}"
Função JS de filtro chamada no oninput Já registrada pelo init.js, sem atributo no campo

Depois de trocar:

  • Apague o tag helper, o partial ou o HTML da busca antiga, o CSS dela e a função JS de filtro.
  • A margem de fora não vem no componente: informe pela class (ex: class="mt-2 mb-3").
  • CSS do projeto que mirava a classe antiga (ex: a busca dentro de uma linha com outro botão) passa a mirar .jarvis-search-bar.
  • O filtro passa a ignorar acentos, e o Enter no filtro local não envia o formulário em volta.

ButtonWhatsApp

Botão flutuante de WhatsApp com animação de pulso. Renderiza um ícone fixo no canto inferior direito que abre uma conversa no WhatsApp.

<button-whats-app number="5511999999999" text="Olá, preciso de ajuda!" />
Propriedade Tipo Descrição
number string Número do WhatsApp com código do país (sem +, espaços ou traços)
text string Texto pré-preenchido na mensagem

Requer o CSS da biblioteca. O ícone é um PNG embutido em base64 e a animação de pulso usa o keyframe jarvis-whatsapp-pulse.


IconHelper

Classe interna (Utils/IconHelper.cs) com os ícones em SVG usados pelos componentes. A biblioteca não depende de nenhuma fonte de ícones: o app pode usar Font Awesome, Bootstrap Icons ou nenhuma, e os ícones internos continuam aparecendo.

Constante Font Awesome Usado em
Plus fa-plus Fab (sem icon) e botão de adicionar
Search fa-magnifying-glass Busca do FormSelect e FormMultiSelect
Caret fa-chevron-down Seta do FormSelect e FormMultiSelect
Upload fa-cloud-arrow-up FormDropzone
Camera fa-camera Botão de câmera do FormDropzone
Calendar fa-calendar-days FormDate e FormDateTime
Clock fa-clock FormTime
ChevronLeft fa-chevron-left Calendar e Pagination
ChevronRight fa-chevron-right Calendar e Pagination
ArrowLeft fa-arrow-left Botão de voltar da Topbar
ArrowRight fa-arrow-right Botão de enviar do SearchBar
Home fa-house Botão de início da Topbar
CircleInfo fa-circle-info Alerta de informação
CircleCheck fa-circle-check Alerta de sucesso
TriangleExclamation fa-triangle-exclamation Alerta de aviso
CircleExclamation fa-circle-exclamation Alerta de erro

O wwwroot/js/init.js tem cópias dos ícones montados no navegador (SELECT_SEARCH_ICON, CALENDAR_CHEVRON_LEFT, CALENDAR_CHEVRON_RIGHT, DROPZONE_FILE_ICON com fa-file e DROPZONE_CLOSE_ICON com fa-xmark). Ao trocar um ícone, troque também a cópia.

A página /Icones do Jarvis.Teste.Web lista todos os ícones do IconHelper lado a lado com o <i> do Font Awesome.

Padrão para novos ícones

Todo SVG novo da biblioteca deve seguir este padrão:

  • Origem: Font Awesome Free 6, estilo solid, com o desenho exato do ícone.
  • viewBox: o do próprio ícone no Font Awesome (0 0 {largura} 512), sem redimensionar para 24x24.
  • Cor: fill="currentColor", para herdar a cor do texto. Sem stroke.
  • Acessibilidade: aria-hidden="true". O texto acessível fica no botão (aria-label).
  • Tamanho: sem width e height no SVG. O tamanho vem do CSS do componente, de preferência height: 1em; width: auto, igual a um <i> do Font Awesome.
  • Classe: só quando o CSS do componente precisar (ex: jarvis-select-caret).
  • Documentação: <summary> com o uso e a classe Font Awesome correspondente (ex: (<c>fa-house</c>)).
/// <summary>
/// Casa, usada no botão de início da barra superior (<c>fa-house</c>).
/// </summary>
internal const string Home = @"<svg viewBox=""0 0 576 512"" fill=""currentColor"" aria-hidden=""true""><path d=""..."" /></svg>";

Extraindo o desenho. O path sai direto da fonte fa-solid-900.ttf (ex: Template.Web.App/wwwroot/webfonts) com o fontTools do Python. O código do ícone (0xF015 para fa-house) está na página do ícone no site do Font Awesome:

pip install --target "$TEMP/fonttools" fonttools

PYTHONPATH="$TEMP/fonttools" python - <<'EOF'
from fontTools.ttLib import TTFont
from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.pens.transformPen import TransformPen

font = TTFont("fa-solid-900.ttf")
glyphs = font.getGlyphSet()
ascent = font["hhea"].ascent

name = font.getBestCmap()[0xF015]
width = font["hmtx"][name][0]
pen = SVGPathPen(glyphs)
glyphs[name].draw(TransformPen(pen, (1, 0, 0, -1, 0, ascent)))

print(f'viewBox="0 0 {width} 512"')
print(pen.getCommands())
EOF

Licença: os ícones do Font Awesome Free são CC BY 4.0 e exigem crédito. O crédito fica no <summary> da classe IconHelper; não remova.


Extensions

Métodos de extensão disponíveis para uso no PageModel e HttpContext.

Validação

if (ModelState.NotValid())
{
    return Page();
}

Listas para Select

Converte uma GenericList<T> do Jarvis.Toolkit em IEnumerable<SelectListItem>, pronta para o asp-items do FormSelect.

Estados = _service.ListarEstados().ToSelectListItem();
<form-select asp-for="IdEstado" asp-items="Model.Estados" />

IP e Dispositivo do Cliente

var ip = HttpContext.GetIpAddress();
var device = HttpContext.GetDevice();

GetIpAddress normaliza endereços IPv4 recebidos em formato mapeado (::ffff:189.10.20.30) para IPv4 puro. Endereços IPv6 reais são mantidos intactos, então a coluna de IP no banco precisa comportar 45 caracteres.

GetDevice retorna o cabeçalho User-Agent já com Trim(). Devolve string vazia quando o cabeçalho não é enviado.

Atrás de Proxy Reverso

Com IIS/ARR, Docker Swarm ou nginx na frente, GetIpAddress devolve o IP do proxy, não o do cliente. Para obter o IP real, registre o processamento dos cabeçalhos encaminhados:

builder.Services.AddJarvisIpCliente();

var app = builder.Build();

app.UseJarvisIpCliente(); // primeira linha do pipeline, antes de auth e HTTPS redirect

Isso configura X-Forwarded-For e X-Forwarded-Proto com ForwardLimit = 1 (um único proxy na frente). Sem parâmetro, aceita cabeçalhos vindos das faixas privadas 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 e loopback.

Informando os proxies explicitamente, somente esses endereços são aceitos:

builder.Services.AddJarvisIpCliente("192.168.1.50");

Segurança: confiar em faixas inteiras permite que qualquer host daquela rede forje o cabeçalho X-Forwarded-For e se passe por outro IP — num cluster Swarm, isso inclui qualquer container. Se o IP for usado para bloqueio, rate limit ou auditoria, passe o endereço do proxy explicitamente.

Notas de infraestrutura:

  • O IIS/ARR envia X-Forwarded-For por padrão, mas não o X-Forwarded-Proto — este exige uma regra de rewrite definindo a server variable HTTP_X_FORWARDED_PROTO. Sem ela, UseHttpsRedirection pode entrar em loop.
  • No Docker Swarm, o ingress faz SNAT e altera apenas o IP de origem (L3); o cabeçalho X-Forwarded-For passa intacto. Como o gateway do ingress fica em 10.0.0.0/8, o modo padrão de publicação funciona. Publicar com mode: host só é necessário para obter o IP real sem depender de cabeçalho.
  • Se um dia entrar outro proxy na frente (Cloudflare, nginx), o ForwardLimit precisa subir para 2.

Cultura na Query String

Por padrão o ASP.NET lê a query string com a cultura invariante. Datas e números em formato brasileiro na URL (?data=13/09/2026&valor=10,50) chegam errados ou nulos no model binding. Para ler com a cultura atual:

builder.Services.AddRazorPages().AddJarvisQueryStringCulture();

Isso troca o QueryStringValueProviderFactory padrão pelo JarvisQueryStringValueProviderFactory, que usa CultureInfo.CurrentCulture. Formulários (POST) não mudam: o ASP.NET já lê o form com a cultura atual.

Migrando código existente. Antes:

public class CulturaQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        var query = context.ActionContext.HttpContext.Request.Query;

        if (query.NotNull() && query.Count > 0)
        {
            context.ValueProviders.Add(new QueryStringValueProvider(BindingSource.Query, query, CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}

builder.Services.AddRazorPages().AddMvcOptions(options =>
{
    var factory = options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>().FirstOrDefault();

    if (factory.NotNull())
    {
        options.ValueProviderFactories[options.ValueProviderFactories.IndexOf(factory)] = new CulturaQueryStringValueProviderFactory();
    }
});

Depois:

builder.Services.AddRazorPages().AddJarvisQueryStringCulture();

Apague a classe CulturaQueryStringValueProviderFactory do projeto. Se o AddRazorPages() já tiver outras configurações, é só encadear:

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AddJarvisRateLimit("/Login", "auth");
}).AddJarvisQueryStringCulture();

Rate Limit

Controle de requisições por política nomeada, particionado por IP do cliente e caminho da requisição.

builder.Services.AddJarvisRateLimit(options =>
{
    options.Message = "Muitas tentativas em pouco tempo. Aguarde alguns minutos e tente de novo.";
    options.AddPolicy("auth", 5, TimeSpan.FromMinutes(1), HttpMethods.Post);
});

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AddJarvisRateLimit("/Login", "auth");
    options.Conventions.AddJarvisRateLimit("/RecuperarSenha", "auth");
});

var app = builder.Build();

app.UseJarvisRateLimit(); // depois de UseRouting, antes de MapRazorPages

Aplicando a política a uma pasta inteira:

options.Conventions.AddJarvisRateLimitFolder("/Conta", "auth");

Ou direto no PageModel, sem convenção:

using Microsoft.AspNetCore.RateLimiting;

[EnableRateLimiting("auth")]
public class LoginModel : PageModel
{
    public void OnGet() { }

    public async Task<IActionResult> OnPostAsync() { }
}

O atributo vale para a página inteira, não por handler — a política é que decide quais métodos HTTP conta. Para liberar uma página dentro de uma pasta limitada, use [DisableRateLimiting].

Com uma política só, dá para registrar direto:

builder.Services.AddJarvisRateLimit("auth", 5, TimeSpan.FromMinutes(1), HttpMethods.Post);

Limitar somente HttpMethods.Post é o normal em Razor Pages: o GET continua exibindo a página e apenas o envio do formulário conta para o limite.

Opções
Propriedade Padrão Descrição
Message "Muitas tentativas…" Mensagem exibida no alerta de erro
StatusCode 429 Status code da resposta bloqueada
RedirectOnRejected true Volta para a página de origem com a mensagem no alerta
OnRejected null Substitui a resposta padrão; quando definido, as opções acima deixam de ser aplicadas
Policies vazio Políticas registradas
Política
Propriedade Padrão Descrição
Name Nome usado ao aplicar a política à página
PermitLimit 5 Requisições permitidas por janela
Window 1 min Duração da janela
QueueLimit 0 Requisições que aguardam na fila em vez de serem rejeitadas
SegmentsPerWindow 0 Acima de 1, troca janela fixa por janela deslizante
Methods null Métodos HTTP limitados. Nulo ou vazio limita todos
IncludePath true Inclui o caminho na chave — cada página tem sua própria contagem
IncludeUser false Inclui o usuário autenticado na chave; anônimos continuam contados por IP

Configuração detalhada:

builder.Services.AddJarvisRateLimit(options =>
{
    options.AddPolicy("conta", policy =>
    {
        policy.PermitLimit = 20;
        policy.Window = TimeSpan.FromMinutes(5);
        policy.SegmentsPerWindow = 5;  // janela deslizante de minuto em minuto
        policy.Methods = [HttpMethods.Post];
        policy.IncludeUser = true;
    });
});
Resposta bloqueada
  • Requisição normal: a mensagem vai para o alerta de erro (TempData) e o usuário volta para a página de origem, onde o componente <alerts> a exibe.
  • Requisição AJAX (X-Requested-With: XMLHttpRequest ou Accept: application/json): JSON no formato ModelResponse.
  • Sem Referer, em GET, ou com RedirectOnRejected = false: status 429 com a mensagem em texto puro.

Em todos os casos o cabeçalho Retry-After é enviado, em segundos.

O redirecionamento devolve 302, não 429 — o status de bloqueio se perde no caminho. Se o cliente depende do 429 (monitoramento, integração), use RedirectOnRejected = false ou um OnRejected próprio.

Contagem em memória: o limite vale por instância da aplicação. Com várias réplicas (Docker Swarm, web farm), o limite efetivo é multiplicado pelo número de réplicas. Para limite compartilhado, use um limitador distribuído (Redis).

A chave de partição usa GetIpAddress(). Atrás de proxy reverso, UseJarvisIpCliente() precisa vir antes no pipeline — sem isso todas as requisições caem na mesma partição, a do IP do proxy.

Autenticação

// Login
await HttpContext.LoginAsync(user);

// Logout
await HttpContext.LogoutAsync();

// Atualizar uma claim
await HttpContext.UpdateClaimAsync("Role", "Admin");

// Atualizar múltiplas claims
await HttpContext.UpdateClaimsAsync(
    new JarvisClaim("Role", "Admin"),
    new JarvisClaim("Name", "João")
);

// Reemitir o cookie a partir de uma identidade já montada
await HttpContext.RefreshLoginAsync(identity);
Método Descrição
LoginAsync Cria a identidade a partir de JarvisAuthenticationUser e autentica
LogoutAsync Encerra a sessão de autenticação
RefreshLoginAsync Reemite o cookie a partir de um ClaimsIdentity existente
UpdateClaimAsync Substitui uma claim e reemite o cookie
UpdateClaimsAsync Substitui várias claims e reemite o cookie

UpdateClaimAsync e UpdateClaimsAsync reemitem o cookie de autenticação — o usuário permanece logado, mas HttpContext.User na requisição atual continua com os valores antigos. A mudança só aparece na próxima requisição.

Todos emitem o cookie com IsPersistent = true, ou seja, a sessão sobrevive ao fechamento do navegador. Não há opção de sessão temporária.

Sessão

// Armazenar string
HttpContext.SetSession("Token", "abc123");

// Armazenar objeto (serializado automaticamente)
HttpContext.SetSession("Carrinho", carrinho);

// Recuperar string
var token = HttpContext.GetSession("Token");

// Recuperar objeto (deserializado automaticamente)
var carrinho = HttpContext.GetSession<Carrinho>("Carrinho");

// Remover
HttpContext.DeleteSession("Token");
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.1 0 9/17/2026
2.0.0.9 66 9/13/2026
2.0.0.8 108 8/19/2026
2.0.0.7 100 8/19/2026
2.0.0.6 104 8/17/2026
2.0.0.5 107 8/16/2026
2.0.0.4 104 8/16/2026
2.0.0.3 100 8/15/2026
2.0.0.2 96 8/10/2026
2.0.0.1 111 8/2/2026
2.0.0 112 8/1/2026
1.0.1.6 117 5/19/2026
1.0.1.5 140 2/1/2026
1.0.1.4 126 1/28/2026
1.0.1.3 223 11/28/2025
1.0.1.2 328 11/13/2025
1.0.1.1 233 10/1/2025
1.0.1 167 7/11/2025
1.0.0.9 348 3/26/2025
1.0.0.8 188 2/13/2025
Loading failed