# Paginação
> Crie paginação estática, dinâmica ou híbrida e personalize completamente os botões de navegação.

- Área: Componentes
- URL humana: https://ninenity.vercel.app/doc/paginacao
- URL Markdown: https://ninenity.vercel.app/doc/markdown/paginacao
- Pacotes e conceitos: Static, Dynamic, Hybrid, Custom controls

## Escolha o tipo certo

| Tipo | Fonte | Melhor uso |
| --- | --- | --- |
| Estática | Lista pronta de templates | Tutoriais, painéis e páginas com layout diferente |
| Dinâmica | Array dividido por `pageSize` | Usuários, logs, ranking e resultados de banco |
| Híbrida | Introdução fixa + array dinâmico | Catálogo com capa ou instruções antes dos itens |

> **Estado é zero-based internamente:** `pageIndex` começa em 0; o valor amigável `context.page` começa em 1. Use cada um para sua finalidade e evite subtrair manualmente em vários lugares.

## Paginação estática

Registre uma lista quando cada página já é conhecida durante o carregamento.

### Discord/Interactions/GuidePages.ts

```text

const page = (title: string, content: string) =>
  new MessageBuilder()
    .addComponents(
      new ContainerBuilder()
        .setAccentColor(0xa571f4)
        .addTextDisplayComponents(
          new TextDisplayBuilder().setContent(
            '## ' + title + '
' + content + '

Página ${pages.current}/${pages.total}'
          )
        )
    )
    .template()

Client.registerPaginator('guide-pages', [
  { id: 'intro', name: 'Introdução', template: page('Introdução', 'Como usar o painel.') },
  { id: 'config', name: 'Configuração', template: page('Configuração', 'Escolha seus canais.') },
  { id: 'finish', name: 'Finalização', template: page('Tudo pronto', 'Revise e confirme.') }
])
```


### Discord/Commands/Guide.ts

```text

Client.slash({
  data: new SlashCommandBuilder()
    .setName('guide')
    .setDescription('Abre o guia paginado.'),
  execute: async interaction => {
    await interaction.paginatorRequest('guide-pages', {
      ephemeral: true,
      pageIndex: 0
    })
  }
})
```


## Paginação dinâmica

O paginator divide `data`, chama `render` para a fatia atual e injeta o contexto da página.

### Discord/Interactions/UserPages.ts

```text

type UserRecord = {
  id: string
  name: string
  level: number
}

const users: UserRecord[] = await userRepository.list()

const paginator = Paginator.dynamic({
  id: 'users-pages',
  data: users,
  pageSize: 5,
  render: async (items, context) => {
    const rows = items.map((user, index) =>
      (context.pageIndex * 5 + index + 1) +
      '. **' + user.name + '** — nível ' + user.level
    )

    return new MessageBuilder()
      .addComponents(
        new ContainerBuilder().addTextDisplayComponents(
          new TextDisplayBuilder().setContent(
            '## Usuários
' + rows.join('
') +
            '

Página ' + context.page + '/' + context.pageCount
          )
        )
      )
      .template()
  }
})

Client.registerPaginator('users-pages', paginator)
```


| Contexto | Valor |
| --- | --- |
| `items` | Itens da página atual |
| `page` | Número amigável, começando em 1 |
| `pageIndex` | Índice interno, começando em 0 |
| `pageCount` | Quantidade total de páginas |
| `totalItems` | Quantidade total de registros |

## Dados novos a cada solicitação

Use uma definição factory para consultar banco ou API quando o usuário abre a paginação, não durante o boot.

### paginator factory

```text

Client.registerPaginator('audit-pages', async context => {
  const guildId = context.guildId
  const records = guildId ? await auditRepository.list(guildId) : []

  return Paginator.dynamic({
    id: 'audit-pages',
    data: records,
    pageSize: 10,
    ownerId: context.ownerId,
    guildId,
    render: (items, page) => new MessageBuilder()
      .addComponents(
        new ContainerBuilder().addTextDisplayComponents(
          new TextDisplayBuilder().setContent(
            '## Auditoria
' +
            items.map(item => '- ' + item.summary).join('
') +
            '

' + page.page + '/' + page.pageCount
          )
        )
      )
      .template()
  })
})
```


> **Snapshot consistente:** Os dados são carregados uma vez ao abrir e mantidos durante aquela instância. Isso evita páginas mudarem de posição no meio da navegação. Para dados realmente vivos, crie uma ação explícita de atualizar.

## Paginação híbrida

A primeira página é fixa; as seguintes vêm de uma coleção dinâmica.

### catalog-paginator.ts

```text

const intro = new MessageBuilder()
  .addComponents(
    new ContainerBuilder()
      .setAccentColor(0x6485ff)
      .addTextDisplayComponents(
        new TextDisplayBuilder().setContent(
          '## Catálogo Ninenity
Use **Próxima** para explorar os projetos.'
        )
      )
  )
  .template()

const paginator = Paginator.hybrid({
  id: 'catalog-pages',
  intro,
  data: projects,
  pageSize: 3,
  render: (items, context) => new MessageBuilder()
    .addComponents(
      new ContainerBuilder().addTextDisplayComponents(
        new TextDisplayBuilder().setContent(
          items.map(project => '### ' + project.name + '
' + project.summary).join('

') +
          '

Página ' + context.page + '/' + context.pageCount
        )
      )
    )
    .template()
})

Client.registerPaginator('catalog-pages', paginator)
```


## Botões de paginação personalizados

Inclua uma action row no próprio template e marque a ação de cada botão. O runtime detecta os controles e não adiciona uma segunda linha.

### custom-controls.ts

```text

const controls = new ActionRowBuilder<MessageActionRowComponentBuilder>()
  .addComponents(
    new ButtonBuilder()
      .setCustomId('guide-pages:first')
      .setLabel('Primeira')
      .setEmoji('⏮')
      .setStyle(ButtonStyle.Secondary)
      .setPaginationAction('first'),
    new ButtonBuilder()
      .setCustomId('guide-pages:back')
      .setLabel('Voltar')
      .setEmoji('◀')
      .setStyle(ButtonStyle.Secondary)
      .setPaginationAction('back'),
    new ButtonBuilder()
      .setCustomId('guide-pages:next')
      .setLabel('Próxima')
      .setEmoji('▶')
      .setStyle(ButtonStyle.Primary)
      .setPaginationAction('next'),
    new ButtonBuilder()
      .setCustomId('guide-pages:last')
      .setLabel('Última')
      .setEmoji('⏭')
      .setStyle(ButtonStyle.Secondary)
      .setPaginationAction('last')
  )

const page = new MessageBuilder()
  .addComponents(
    new ContainerBuilder()
      .addTextDisplayComponents(
        new TextDisplayBuilder().setContent(
          'Página ${pages.current} de ${pages.total}'
        )
      )
      .addActionRowComponents(controls)
  )
  .template()
```


- O prefixo do `customId` deve ser exatamente o ID registrado no paginator.
- `back` e `previous` são entendidos como a página anterior; no builder use `setPaginationAction('back')`.
- Use `first` e `last` apenas quando a quantidade de páginas justifica esses atalhos.
- O runtime hidrata os tokens `pages.current` e `pages.total` antes de enviar.

## Personalizar rótulos automáticos

Se as páginas não incluem controles, `Paginator` cria a linha e você pode trocar os rótulos.

### automatic-controls.ts

```text

const paginator = Paginator.static({
  id: 'help-pages',
  pages: [introPage, commandsPage, settingsPage],
  controls: {
    first: 'Início',
    previous: 'Voltar',
    next: 'Avançar',
    last: 'Fim'
  }
})

Client.registerPaginator('help-pages', paginator)
```


> **Não duplique controles:** Escolha uma estratégia: controles gerados pelo Paginator ou action row customizada no template. Se a linha personalizada usar outro ID, ela será tratada como callback comum e a linha automática também aparecerá.
