{"name":"Ninenity","description":"Documentação oficial do Ninenity Core, ComponentBuilder e runtime para bots Discord.","format":"ninenity-docs/v1","resources":{"index":"https://ninenity.vercel.app/llms.txt","fullMarkdown":"https://ninenity.vercel.app/llms-full.txt","humanDocs":"https://ninenity.vercel.app/doc","repository":"https://github.com/ninenity"},"pages":[{"slug":"comecando","title":"Começando com o Core","shortTitle":"Começando","group":"Base do Core","eyebrow":"BASE DO CORE / PRIMEIROS PASSOS","description":"Entenda o papel do runtime, a estrutura do Template Bot e o caminho percorrido até o primeiro comando responder.","badges":["@ninenity/core","TypeScript","Discord.js"],"sections":[{"id":"modelo-mental","title":"O modelo mental","description":"O Core não substitui o Discord.js. Ele organiza o ciclo de vida ao redor dele.","blocks":[{"kind":"cards","columns":3,"items":[{"icon":"1","title":"Você declara","text":"Cada arquivo registra um comando, evento, task ou interação no `Client`."},{"icon":"2","title":"O Core carrega","text":"O auto-loader percorre `settings.baseDir`, importa os módulos e valida os registros."},{"icon":"3","title":"O runtime executa","text":"Cooldown, fila, contexto de guild e respostas são preparados antes do seu `execute`."}]},{"kind":"callout","tone":"info","title":"Regra mais importante","text":"Um arquivo deve representar uma responsabilidade. Evite concentrar todos os comandos em `Bot.ts`; deixe o auto-loader fazer o trabalho.","icon":"✦"}]},{"id":"estrutura","title":"Estrutura do Template Bot","description":"A estrutura já separa código carregado automaticamente de helpers importados explicitamente.","blocks":[{"kind":"code","title":"estrutura do projeto","language":"text","code":"\nsrc/\n├─ Bot.ts                         # ponto de entrada\n├─ Discord/\n│  ├─ Commands/                  # slash, prefixo e menus de contexto\n│  ├─ Events/                    # eventos do Discord.js\n│  ├─ Interactions/              # modais, callbacks, prompts e paginações\n│  └─ Tasks/                     # rotinas periódicas\n├─ Shared/\n│  ├─ Settings/Client.ts         # LibsClient, token e intents\n│  ├─ Settings/Database.ts       # plugin opcional de banco\n│  ├─ Emojis/                    # emojis do aplicativo\n│  └─ Translate/                 # idiomas\n└─ Utils/                        # regras e helpers importados por módulos\n          "},{"kind":"table","columns":["Pasta","Carregamento","Use para"],"rows":[["`Discord/Commands`","Automático","Comandos slash, prefixados e menus de contexto"],["`Discord/Events`","Automático","Eventos emitidos pelo Discord.js"],["`Discord/Interactions`","Automático","Modais, prompts, paginações e callbacks registrados"],["`Discord/Tasks`","Automático","Rotinas globais ou executadas por servidor"],["`Shared` e `Utils`","Explícito","Configuração, serviços e regras reutilizáveis"]]}]},{"id":"client","title":"Configure o Client","description":"O `LibsClient` estende o cliente do Discord.js e adiciona os registradores do ecossistema.","blocks":[{"kind":"code","title":"src/Shared/Settings/Client.ts","code":"\nimport {\n  GatewayIntentBits,\n  LibsClient,\n  type AppConfig\n} from '@ninenity/core'\n\nconst Client = new LibsClient<AppConfig>({\n  intents: [\n    GatewayIntentBits.Guilds,\n    GatewayIntentBits.GuildMessages,\n    GatewayIntentBits.MessageContent,\n    GatewayIntentBits.GuildMembers\n  ],\n  token: process.env.BOT_TOKEN || process.env.DISCORD_TOKEN,\n  config: {}\n})\n\nexport default Client\n          "},{"kind":"callout","tone":"warning","title":"Intents precisam combinar com o portal","text":"Ativar `MessageContent` ou `GuildMembers` no código não basta. Os intents privilegiados também precisam estar habilitados no Discord Developer Portal.","icon":"!"},{"kind":"table","columns":["Intent","Necessário quando"],"rows":[["`Guilds`","Quase sempre: slash commands, canais, cargos e configuração por servidor"],["`GuildMessages`","O bot observa mensagens ou usa comandos prefixados"],["`MessageContent`","O conteúdo textual das mensagens precisa ser lido"],["`GuildMembers`","Entradas, saídas, cargos ou dados completos de membros são usados"]]}]},{"id":"inicializacao","title":"Inicialize o runtime","description":"`Ninenity.init()` prepara plugins e auto-loader antes de `Client.login()` abrir a conexão com o Discord.","blocks":[{"kind":"code","title":"src/Bot.ts","code":"\nimport 'dotenv/config'\nimport { Ninenity } from '@ninenity/core'\nimport Client from './Shared/Settings/Client'\nimport { databasePlugin } from './Shared/Settings/Database'\n\nNinenity.init({\n  bots: [Client],\n  plugins: databasePlugin ? { database: databasePlugin } : undefined,\n  settings: { baseDir: __dirname }\n})\n\nClient.login()\n          "},{"kind":"steps","items":[{"title":"Variáveis são carregadas","text":"O import de `dotenv/config` disponibiliza o token e integrações antes de criar o Client."},{"title":"O runtime recebe os bots","text":"`bots` aceita mais de um Client, mas cada bot mantém seus próprios registradores e timers."},{"title":"Módulos são encontrados","text":"`baseDir` aponta para `src` em desenvolvimento e para `dist` após o build."},{"title":"O login começa","text":"Depois dos registros, `Client.login()` conecta e conclui as etapas de bootstrap."}]}]},{"id":"primeiro-modulo","title":"Crie o primeiro módulo","description":"O arquivo só precisa importar o Client compartilhado e fazer seu registro no escopo do módulo.","blocks":[{"kind":"code","title":"src/Discord/Commands/Ping.ts","code":"\nimport {\n  SlashCommandBuilder,\n  type SlashInputCommandInteraction\n} from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('ping')\n    .setDescription('Mostra a latência atual.'),\n  cooldown: '3s',\n  execute: async (interaction: SlashInputCommandInteraction) => {\n    await interaction.reply('Pong!')\n  }\n})\n          "},{"kind":"text","text":"Você não importa `Ping.ts` em `Bot.ts`. O auto-loader encontra o arquivo, executa o módulo uma vez e o `Client.slash()` guarda a definição para deploy e execução."},{"kind":"callout","tone":"success","title":"Como saber se funcionou","text":"Ao iniciar, confirme que o boot termina, o comando aparece entre os registros e o Client fica online. Se o comando não aparecer no Discord, consulte **Solução de problemas**.","icon":"✓"}]},{"id":"ambiente","title":"Variáveis de ambiente","description":"Comece com o mínimo e adicione integrações somente quando forem usadas.","blocks":[{"kind":"code","title":".env","language":"dotenv","code":"\nBOT_TOKEN=seu_token_do_discord\n\n# Opcionais: integração com API e HUB\nAPI_KEY=\nAPI_SECRET=\n\n# Opcional: banco de dados\nDATABASE_URL=\n          "},{"kind":"list","items":["Nunca envie `.env` para o Git; mantenha apenas um `.env.example` sem secrets.","Use um token de bot, não o client secret da aplicação.","A ausência de `API_KEY` e `API_SECRET` desativa a hidratação remota, mas não impede o bot de conectar ao Discord.","Mantenha `src` e `dist` com a mesma árvore para o auto-loader funcionar após o build."]}]}],"url":"https://ninenity.vercel.app/doc/comecando","markdownUrl":"https://ninenity.vercel.app/doc/markdown/comecando","markdown":"# Começando com o Core\n> Entenda o papel do runtime, a estrutura do Template Bot e o caminho percorrido até o primeiro comando responder.\n\n- Área: Base do Core\n- URL humana: https://ninenity.vercel.app/doc/comecando\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/comecando\n- Pacotes e conceitos: @ninenity/core, TypeScript, Discord.js\n\n## O modelo mental\n\nO Core não substitui o Discord.js. Ele organiza o ciclo de vida ao redor dele.\n\n- **Você declara**: Cada arquivo registra um comando, evento, task ou interação no `Client`.\n- **O Core carrega**: O auto-loader percorre `settings.baseDir`, importa os módulos e valida os registros.\n- **O runtime executa**: Cooldown, fila, contexto de guild e respostas são preparados antes do seu `execute`.\n\n> **Regra mais importante:** Um arquivo deve representar uma responsabilidade. Evite concentrar todos os comandos em `Bot.ts`; deixe o auto-loader fazer o trabalho.\n\n## Estrutura do Template Bot\n\nA estrutura já separa código carregado automaticamente de helpers importados explicitamente.\n\n### estrutura do projeto\n\n```text\n\nsrc/\n├─ Bot.ts                         # ponto de entrada\n├─ Discord/\n│  ├─ Commands/                  # slash, prefixo e menus de contexto\n│  ├─ Events/                    # eventos do Discord.js\n│  ├─ Interactions/              # modais, callbacks, prompts e paginações\n│  └─ Tasks/                     # rotinas periódicas\n├─ Shared/\n│  ├─ Settings/Client.ts         # LibsClient, token e intents\n│  ├─ Settings/Database.ts       # plugin opcional de banco\n│  ├─ Emojis/                    # emojis do aplicativo\n│  └─ Translate/                 # idiomas\n└─ Utils/                        # regras e helpers importados por módulos\n```\n\n\n| Pasta | Carregamento | Use para |\n| --- | --- | --- |\n| `Discord/Commands` | Automático | Comandos slash, prefixados e menus de contexto |\n| `Discord/Events` | Automático | Eventos emitidos pelo Discord.js |\n| `Discord/Interactions` | Automático | Modais, prompts, paginações e callbacks registrados |\n| `Discord/Tasks` | Automático | Rotinas globais ou executadas por servidor |\n| `Shared` e `Utils` | Explícito | Configuração, serviços e regras reutilizáveis |\n\n## Configure o Client\n\nO `LibsClient` estende o cliente do Discord.js e adiciona os registradores do ecossistema.\n\n### src/Shared/Settings/Client.ts\n\n```text\n\nimport {\n  GatewayIntentBits,\n  LibsClient,\n  type AppConfig\n} from '@ninenity/core'\n\nconst Client = new LibsClient<AppConfig>({\n  intents: [\n    GatewayIntentBits.Guilds,\n    GatewayIntentBits.GuildMessages,\n    GatewayIntentBits.MessageContent,\n    GatewayIntentBits.GuildMembers\n  ],\n  token: process.env.BOT_TOKEN || process.env.DISCORD_TOKEN,\n  config: {}\n})\n\nexport default Client\n```\n\n\n> **Intents precisam combinar com o portal:** Ativar `MessageContent` ou `GuildMembers` no código não basta. Os intents privilegiados também precisam estar habilitados no Discord Developer Portal.\n\n| Intent | Necessário quando |\n| --- | --- |\n| `Guilds` | Quase sempre: slash commands, canais, cargos e configuração por servidor |\n| `GuildMessages` | O bot observa mensagens ou usa comandos prefixados |\n| `MessageContent` | O conteúdo textual das mensagens precisa ser lido |\n| `GuildMembers` | Entradas, saídas, cargos ou dados completos de membros são usados |\n\n## Inicialize o runtime\n\n`Ninenity.init()` prepara plugins e auto-loader antes de `Client.login()` abrir a conexão com o Discord.\n\n### src/Bot.ts\n\n```text\n\nimport 'dotenv/config'\nimport { Ninenity } from '@ninenity/core'\nimport Client from './Shared/Settings/Client'\nimport { databasePlugin } from './Shared/Settings/Database'\n\nNinenity.init({\n  bots: [Client],\n  plugins: databasePlugin ? { database: databasePlugin } : undefined,\n  settings: { baseDir: __dirname }\n})\n\nClient.login()\n```\n\n\n1. **Variáveis são carregadas** — O import de `dotenv/config` disponibiliza o token e integrações antes de criar o Client.\n2. **O runtime recebe os bots** — `bots` aceita mais de um Client, mas cada bot mantém seus próprios registradores e timers.\n3. **Módulos são encontrados** — `baseDir` aponta para `src` em desenvolvimento e para `dist` após o build.\n4. **O login começa** — Depois dos registros, `Client.login()` conecta e conclui as etapas de bootstrap.\n\n## Crie o primeiro módulo\n\nO arquivo só precisa importar o Client compartilhado e fazer seu registro no escopo do módulo.\n\n### src/Discord/Commands/Ping.ts\n\n```text\n\nimport {\n  SlashCommandBuilder,\n  type SlashInputCommandInteraction\n} from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('ping')\n    .setDescription('Mostra a latência atual.'),\n  cooldown: '3s',\n  execute: async (interaction: SlashInputCommandInteraction) => {\n    await interaction.reply('Pong!')\n  }\n})\n```\n\n\nVocê não importa `Ping.ts` em `Bot.ts`. O auto-loader encontra o arquivo, executa o módulo uma vez e o `Client.slash()` guarda a definição para deploy e execução.\n\n> **Como saber se funcionou:** Ao iniciar, confirme que o boot termina, o comando aparece entre os registros e o Client fica online. Se o comando não aparecer no Discord, consulte **Solução de problemas**.\n\n## Variáveis de ambiente\n\nComece com o mínimo e adicione integrações somente quando forem usadas.\n\n### .env\n\n```dotenv\n\nBOT_TOKEN=seu_token_do_discord\n\n# Opcionais: integração com API e HUB\nAPI_KEY=\nAPI_SECRET=\n\n# Opcional: banco de dados\nDATABASE_URL=\n```\n\n\n- Nunca envie `.env` para o Git; mantenha apenas um `.env.example` sem secrets.\n- Use um token de bot, não o client secret da aplicação.\n- A ausência de `API_KEY` e `API_SECRET` desativa a hidratação remota, mas não impede o bot de conectar ao Discord.\n- Mantenha `src` e `dist` com a mesma árvore para o auto-loader funcionar após o build.\n"},{"slug":"comandos","title":"Comandos","group":"Base do Core","eyebrow":"BASE DO CORE / COMANDOS","description":"Crie comandos slash, prefixados e menus de contexto com um contrato de execução consistente.","badges":["Slash","Prefix","Context menu"],"sections":[{"id":"contrato","title":"Um contrato, três entradas","description":"Todo registro combina os dados visíveis ao Discord com opções de runtime e uma função `execute`.","blocks":[{"kind":"table","columns":["Registro","Entrada","Builder"],"rows":[["`Client.slash()`","Comando nativo `/nome`","`SlashCommandBuilder`"],["`Client.prefix()`","Mensagem como `!nome`","`PrefixCommandBuilder`"],["`Client.contextMenu()`","Menu sobre usuário ou mensagem","`ContextMenuCommandBuilder`"]]},{"kind":"cards","columns":3,"items":[{"icon":"D","title":"data","text":"Nome, descrição, opções, aliases ou tipo do comando."},{"icon":"R","title":"runtime","text":"Cooldown, fila, escopo de DM e outras regras antes da execução."},{"icon":"E","title":"execute","text":"A lógica chamada com a interação já normalizada pelo Core."}]}]},{"id":"slash","title":"Comando slash","description":"Use slash para descoberta nativa, validação de opções e autocomplete do Discord.","blocks":[{"kind":"code","title":"Discord/Commands/Ping.ts","code":"\nimport {\n  SlashCommandBuilder,\n  type SlashInputCommandInteraction\n} from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('ping')\n    .setDescription('Responde com a latência.'),\n  cooldown: '5s',\n  queue: 'user',\n  execute: async (interaction: SlashInputCommandInteraction) => {\n    const sent = await interaction.reply({\n      content: 'Calculando...',\n      withResponse: true\n    })\n\n    const latency = sent.resource?.message?.createdTimestamp\n      ? sent.resource.message.createdTimestamp - interaction.createdTimestamp\n      : 0\n\n    await interaction.editReply('Latência: ' + latency + 'ms')\n  }\n})\n          "},{"kind":"callout","tone":"info","title":"Responda em até três segundos","text":"Se o trabalho pode demorar, chame `interaction.deferReply()` primeiro e finalize com `editReply()`. Isso evita a mensagem de interação expirada.","icon":"i"}]},{"id":"opcoes","title":"Opções tipadas","description":"O builder declara as opções e o objeto `interaction.options` entrega os valores validados.","blocks":[{"kind":"code","title":"Discord/Commands/Inspect.ts","code":"\nimport {\n  ChannelType,\n  SlashCommandBuilder,\n  type SlashInputCommandInteraction\n} from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('inspect')\n    .setDescription('Inspeciona dados informados.')\n    .addStringOption(option => option\n      .setName('texto')\n      .setDescription('Texto obrigatório')\n      .setRequired(true))\n    .addIntegerOption(option => option\n      .setName('quantidade')\n      .setDescription('Entre 1 e 5')\n      .setMinValue(1)\n      .setMaxValue(5))\n    .addUserOption(option => option\n      .setName('usuario')\n      .setDescription('Usuário opcional'))\n    .addChannelOption(option => option\n      .setName('canal')\n      .setDescription('Apenas canais de texto')\n      .addChannelTypes(ChannelType.GuildText)),\n  config: { Dm: false },\n  execute: async (interaction: SlashInputCommandInteraction) => {\n    const text = interaction.options.getString('texto', true)\n    const count = interaction.options.getInteger('quantidade') ?? 1\n    const user = interaction.options.getUser('usuario')\n    const channel = interaction.options.getChannel('canal')\n\n    await interaction.reply([\n      'Texto: ' + text.repeat(count),\n      'Usuário: ' + (user?.tag ?? 'não informado'),\n      'Canal: ' + (channel?.name ?? 'não informado')\n    ].join('\n'))\n  }\n})\n          "},{"kind":"table","columns":["Opção","Leitura"],"rows":[["String","`getString(nome, obrigatório?)`"],["Integer / Number","`getInteger()` / `getNumber()`"],["Boolean","`getBoolean()`"],["User / Member","`getUser()` / `getMember()`"],["Channel / Role","`getChannel()` / `getRole()`"],["Attachment","`getAttachment()`"]]}]},{"id":"executor-compartilhado","title":"Slash e prefixo com o mesmo executor","description":"Quando a regra é igual, extraia uma função que aceite a interação normalizada e registre duas entradas.","blocks":[{"kind":"code","title":"Discord/Commands/Starter.ts","code":"\nimport {\n  PrefixCommandBuilder,\n  SlashCommandBuilder,\n  type InputCommandInteraction\n} from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nconst execute = async (interaction: InputCommandInteraction) => {\n  await interaction.reply('Olá, ' + interaction.user.username + '!')\n}\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('starter')\n    .setDescription('Mostra a resposta inicial.'),\n  cooldown: '3s',\n  queue: 'user',\n  execute\n})\n\nClient.prefix({\n  data: new PrefixCommandBuilder()\n    .setName('starter')\n    .setAliases(['start']),\n  cooldown: '3s',\n  queue: 'user',\n  execute\n})\n          "},{"kind":"callout","tone":"warning","title":"Use apenas a superfície comum","text":"Dentro do executor compartilhado, evite APIs exclusivas de slash ou prefixo. Quando precisar delas, estreite o tipo ou mantenha executores separados.","icon":"!"}]},{"id":"prefixo","title":"Prefixo, aliases e subcomandos","description":"O PrefixCommandBuilder oferece a mesma leitura estruturada de opções, sem depender do parser manual de `message.content`.","blocks":[{"kind":"code","title":"Discord/Commands/Manage.ts","code":"\nimport {\n  PrefixCommandBuilder,\n  type PrefixInputCommandInteraction\n} from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.prefix({\n  data: new PrefixCommandBuilder()\n    .setName('manage')\n    .setAliases(['m'])\n    .addSubcommand(subcommand => subcommand\n      .setName('status')\n      .setDescription('Mostra o status'))\n    .addSubcommand(subcommand => subcommand\n      .setName('set')\n      .setDescription('Salva um valor')\n      .addStringOption(option => option\n        .setName('value')\n        .setDescription('Novo valor')\n        .setRequired(true)))\n    .addSubcommandGroup(group => group\n      .setName('config')\n      .setDescription('Configuração')\n      .addSubcommand(subcommand => subcommand\n        .setName('show')\n        .setDescription('Exibe a configuração'))),\n  execute: async (interaction: PrefixInputCommandInteraction) => {\n    const group = interaction.options.getSubcommandGroup()\n    const command = interaction.options.getSubcommand() ?? 'status'\n    const value = interaction.options.getString('value')\n\n    await interaction.reply(\n      'grupo=' + (group ?? 'geral') +\n      ' comando=' + command +\n      ' valor=' + (value ?? 'nenhum')\n    )\n  }\n})\n          "},{"kind":"text","text":"Com prefixo `!`, os exemplos seriam `!manage status`, `!m set produção` e `!manage config show`. O Core resolve aliases, grupo, subcomando e opções antes de chamar `execute`."}]},{"id":"context-menu","title":"Menus de contexto","description":"Menus de contexto aparecem no clique direito sobre um usuário ou uma mensagem.","blocks":[{"kind":"code","title":"Discord/Commands/UserContext.ts","code":"\nimport {\n  ApplicationCommandType,\n  ContextMenuCommandBuilder,\n  type ContextMenuInputCommandInteraction\n} from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.contextMenu({\n  data: new ContextMenuCommandBuilder()\n    .setName('Ver usuário')\n    .setType(ApplicationCommandType.User),\n  execute: async (interaction: ContextMenuInputCommandInteraction) => {\n    const raw = interaction.interaction\n    const target = raw.isUserContextMenuCommand() ? raw.targetUser : null\n\n    await interaction.reply({\n      content: target ? 'Selecionado: ' + target.tag : 'Usuário indisponível.',\n      flags: 64\n    })\n  }\n})\n          "},{"kind":"code","title":"Discord/Commands/MessageContext.ts","code":"\nClient.contextMenu({\n  data: new ContextMenuCommandBuilder()\n    .setName('Citar mensagem')\n    .setType(ApplicationCommandType.Message),\n  execute: async interaction => {\n    const raw = interaction.interaction\n    const target = raw.isMessageContextMenuCommand()\n      ? raw.targetMessage\n      : null\n\n    await interaction.reply(\n      target ? target.author.tag + ': ' + target.content : 'Mensagem indisponível.'\n    )\n  }\n})\n          "}]},{"id":"controle-execucao","title":"Cooldown e filas","description":"Cooldown limita frequência; fila evita duas execuções concorrentes sobre o mesmo recurso.","blocks":[{"kind":"code","title":"opções declarativas","code":"\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('sincronizar')\n    .setDescription('Sincroniza os dados do usuário.'),\n  cooldown: '30s',\n  queue: 'user',\n  execute: async interaction => {\n    await syncUser(interaction.user.id)\n    await interaction.reply({ content: 'Sincronizado.', flags: 64 })\n  }\n})\n          "},{"kind":"code","title":"fila manual dentro de um handler","code":"\nawait Client.queue('billing:' + interaction.user.id, async () => {\n  const invoice = await createInvoice(interaction.user.id)\n  await interaction.reply('Fatura criada: ' + invoice.id)\n})\n          "},{"kind":"callout","tone":"info","title":"Isolamento automático","text":"Quando `Client.queue()` roda dentro do contexto de uma guild, o Core inclui essa guild na chave interna. A mesma ação em dois servidores não bloqueia uma à outra.","icon":"◎"}]}],"url":"https://ninenity.vercel.app/doc/comandos","markdownUrl":"https://ninenity.vercel.app/doc/markdown/comandos","markdown":"# Comandos\n> Crie comandos slash, prefixados e menus de contexto com um contrato de execução consistente.\n\n- Área: Base do Core\n- URL humana: https://ninenity.vercel.app/doc/comandos\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/comandos\n- Pacotes e conceitos: Slash, Prefix, Context menu\n\n## Um contrato, três entradas\n\nTodo registro combina os dados visíveis ao Discord com opções de runtime e uma função `execute`.\n\n| Registro | Entrada | Builder |\n| --- | --- | --- |\n| `Client.slash()` | Comando nativo `/nome` | `SlashCommandBuilder` |\n| `Client.prefix()` | Mensagem como `!nome` | `PrefixCommandBuilder` |\n| `Client.contextMenu()` | Menu sobre usuário ou mensagem | `ContextMenuCommandBuilder` |\n\n- **data**: Nome, descrição, opções, aliases ou tipo do comando.\n- **runtime**: Cooldown, fila, escopo de DM e outras regras antes da execução.\n- **execute**: A lógica chamada com a interação já normalizada pelo Core.\n\n## Comando slash\n\nUse slash para descoberta nativa, validação de opções e autocomplete do Discord.\n\n### Discord/Commands/Ping.ts\n\n```text\n\nimport {\n  SlashCommandBuilder,\n  type SlashInputCommandInteraction\n} from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('ping')\n    .setDescription('Responde com a latência.'),\n  cooldown: '5s',\n  queue: 'user',\n  execute: async (interaction: SlashInputCommandInteraction) => {\n    const sent = await interaction.reply({\n      content: 'Calculando...',\n      withResponse: true\n    })\n\n    const latency = sent.resource?.message?.createdTimestamp\n      ? sent.resource.message.createdTimestamp - interaction.createdTimestamp\n      : 0\n\n    await interaction.editReply('Latência: ' + latency + 'ms')\n  }\n})\n```\n\n\n> **Responda em até três segundos:** Se o trabalho pode demorar, chame `interaction.deferReply()` primeiro e finalize com `editReply()`. Isso evita a mensagem de interação expirada.\n\n## Opções tipadas\n\nO builder declara as opções e o objeto `interaction.options` entrega os valores validados.\n\n### Discord/Commands/Inspect.ts\n\n```text\n\nimport {\n  ChannelType,\n  SlashCommandBuilder,\n  type SlashInputCommandInteraction\n} from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('inspect')\n    .setDescription('Inspeciona dados informados.')\n    .addStringOption(option => option\n      .setName('texto')\n      .setDescription('Texto obrigatório')\n      .setRequired(true))\n    .addIntegerOption(option => option\n      .setName('quantidade')\n      .setDescription('Entre 1 e 5')\n      .setMinValue(1)\n      .setMaxValue(5))\n    .addUserOption(option => option\n      .setName('usuario')\n      .setDescription('Usuário opcional'))\n    .addChannelOption(option => option\n      .setName('canal')\n      .setDescription('Apenas canais de texto')\n      .addChannelTypes(ChannelType.GuildText)),\n  config: { Dm: false },\n  execute: async (interaction: SlashInputCommandInteraction) => {\n    const text = interaction.options.getString('texto', true)\n    const count = interaction.options.getInteger('quantidade') ?? 1\n    const user = interaction.options.getUser('usuario')\n    const channel = interaction.options.getChannel('canal')\n\n    await interaction.reply([\n      'Texto: ' + text.repeat(count),\n      'Usuário: ' + (user?.tag ?? 'não informado'),\n      'Canal: ' + (channel?.name ?? 'não informado')\n    ].join('\n'))\n  }\n})\n```\n\n\n| Opção | Leitura |\n| --- | --- |\n| String | `getString(nome, obrigatório?)` |\n| Integer / Number | `getInteger()` / `getNumber()` |\n| Boolean | `getBoolean()` |\n| User / Member | `getUser()` / `getMember()` |\n| Channel / Role | `getChannel()` / `getRole()` |\n| Attachment | `getAttachment()` |\n\n## Slash e prefixo com o mesmo executor\n\nQuando a regra é igual, extraia uma função que aceite a interação normalizada e registre duas entradas.\n\n### Discord/Commands/Starter.ts\n\n```text\n\nimport {\n  PrefixCommandBuilder,\n  SlashCommandBuilder,\n  type InputCommandInteraction\n} from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nconst execute = async (interaction: InputCommandInteraction) => {\n  await interaction.reply('Olá, ' + interaction.user.username + '!')\n}\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('starter')\n    .setDescription('Mostra a resposta inicial.'),\n  cooldown: '3s',\n  queue: 'user',\n  execute\n})\n\nClient.prefix({\n  data: new PrefixCommandBuilder()\n    .setName('starter')\n    .setAliases(['start']),\n  cooldown: '3s',\n  queue: 'user',\n  execute\n})\n```\n\n\n> **Use apenas a superfície comum:** Dentro do executor compartilhado, evite APIs exclusivas de slash ou prefixo. Quando precisar delas, estreite o tipo ou mantenha executores separados.\n\n## Prefixo, aliases e subcomandos\n\nO PrefixCommandBuilder oferece a mesma leitura estruturada de opções, sem depender do parser manual de `message.content`.\n\n### Discord/Commands/Manage.ts\n\n```text\n\nimport {\n  PrefixCommandBuilder,\n  type PrefixInputCommandInteraction\n} from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.prefix({\n  data: new PrefixCommandBuilder()\n    .setName('manage')\n    .setAliases(['m'])\n    .addSubcommand(subcommand => subcommand\n      .setName('status')\n      .setDescription('Mostra o status'))\n    .addSubcommand(subcommand => subcommand\n      .setName('set')\n      .setDescription('Salva um valor')\n      .addStringOption(option => option\n        .setName('value')\n        .setDescription('Novo valor')\n        .setRequired(true)))\n    .addSubcommandGroup(group => group\n      .setName('config')\n      .setDescription('Configuração')\n      .addSubcommand(subcommand => subcommand\n        .setName('show')\n        .setDescription('Exibe a configuração'))),\n  execute: async (interaction: PrefixInputCommandInteraction) => {\n    const group = interaction.options.getSubcommandGroup()\n    const command = interaction.options.getSubcommand() ?? 'status'\n    const value = interaction.options.getString('value')\n\n    await interaction.reply(\n      'grupo=' + (group ?? 'geral') +\n      ' comando=' + command +\n      ' valor=' + (value ?? 'nenhum')\n    )\n  }\n})\n```\n\n\nCom prefixo `!`, os exemplos seriam `!manage status`, `!m set produção` e `!manage config show`. O Core resolve aliases, grupo, subcomando e opções antes de chamar `execute`.\n\n## Menus de contexto\n\nMenus de contexto aparecem no clique direito sobre um usuário ou uma mensagem.\n\n### Discord/Commands/UserContext.ts\n\n```text\n\nimport {\n  ApplicationCommandType,\n  ContextMenuCommandBuilder,\n  type ContextMenuInputCommandInteraction\n} from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.contextMenu({\n  data: new ContextMenuCommandBuilder()\n    .setName('Ver usuário')\n    .setType(ApplicationCommandType.User),\n  execute: async (interaction: ContextMenuInputCommandInteraction) => {\n    const raw = interaction.interaction\n    const target = raw.isUserContextMenuCommand() ? raw.targetUser : null\n\n    await interaction.reply({\n      content: target ? 'Selecionado: ' + target.tag : 'Usuário indisponível.',\n      flags: 64\n    })\n  }\n})\n```\n\n\n### Discord/Commands/MessageContext.ts\n\n```text\n\nClient.contextMenu({\n  data: new ContextMenuCommandBuilder()\n    .setName('Citar mensagem')\n    .setType(ApplicationCommandType.Message),\n  execute: async interaction => {\n    const raw = interaction.interaction\n    const target = raw.isMessageContextMenuCommand()\n      ? raw.targetMessage\n      : null\n\n    await interaction.reply(\n      target ? target.author.tag + ': ' + target.content : 'Mensagem indisponível.'\n    )\n  }\n})\n```\n\n\n## Cooldown e filas\n\nCooldown limita frequência; fila evita duas execuções concorrentes sobre o mesmo recurso.\n\n### opções declarativas\n\n```text\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('sincronizar')\n    .setDescription('Sincroniza os dados do usuário.'),\n  cooldown: '30s',\n  queue: 'user',\n  execute: async interaction => {\n    await syncUser(interaction.user.id)\n    await interaction.reply({ content: 'Sincronizado.', flags: 64 })\n  }\n})\n```\n\n\n### fila manual dentro de um handler\n\n```text\n\nawait Client.queue('billing:' + interaction.user.id, async () => {\n  const invoice = await createInvoice(interaction.user.id)\n  await interaction.reply('Fatura criada: ' + invoice.id)\n})\n```\n\n\n> **Isolamento automático:** Quando `Client.queue()` roda dentro do contexto de uma guild, o Core inclui essa guild na chave interna. A mesma ação em dois servidores não bloqueia uma à outra.\n"},{"slug":"interacoes","title":"Interações","group":"Base do Core","eyebrow":"BASE DO CORE / INTERAÇÕES","description":"Entenda respostas, modais, autocomplete e o contexto normalizado usado em todos os fluxos interativos.","badges":["Replies","Modal","Autocomplete"],"sections":[{"id":"ciclo-resposta","title":"O ciclo de uma resposta","description":"Uma interação pode receber uma resposta inicial e depois ser editada ou acompanhada por mensagens adicionais.","blocks":[{"kind":"table","columns":["Método","Quando usar"],"rows":[["`reply()`","Primeira resposta imediata"],["`deferReply()`","Reserva a resposta quando o trabalho demora"],["`editReply()`","Finaliza ou atualiza a resposta reservada"],["`followUp()`","Envia uma resposta adicional"],["`deleteReply()`","Remove a resposta original"]]},{"kind":"code","title":"resposta demorada","code":"\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('relatorio')\n    .setDescription('Gera um relatório detalhado.'),\n  execute: async interaction => {\n    await interaction.deferReply({ flags: 64 })\n\n    const report = await generateReport(interaction.guildId)\n\n    await interaction.editReply('Relatório pronto: ' + report.url)\n    await interaction.followUp({\n      content: 'A exportação expira em 24 horas.',\n      flags: 64\n    })\n  }\n})\n          "}]},{"id":"contexto-normalizado","title":"Contexto normalizado","description":"O Core fornece uma superfície comum e mantém a interação original em `interaction.interaction` quando você precisa estreitar o tipo.","blocks":[{"kind":"cards","items":[{"icon":"U","title":"Identidade","text":"`user`, `member`, `guild`, `guildId`, `channel` e `channelId`."},{"icon":"R","title":"Resposta","text":"`reply`, `deferReply`, `editReply`, `followUp` e estado da resposta."},{"icon":"C","title":"Core","text":"`promptRequest`, `paginatorRequest`, configuração e contexto isolado."},{"icon":"D","title":"Discord.js","text":"A propriedade `interaction` preserva a interação nativa completa."}]},{"kind":"code","title":"acesso à configuração da guild atual","code":"\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('canal-suporte')\n    .setDescription('Mostra o canal configurado.'),\n  execute: async interaction => {\n    const config = Client.appConfig()\n    const supportChannel = Client.getAppChannels().support\n\n    await interaction.reply({\n      content: 'Canal: ' + (supportChannel?.toString() ?? 'não configurado'),\n      flags: 64\n    })\n  }\n})\n          "}]},{"id":"abrir-modal","title":"Abrindo um modal","description":"O comando constrói o modal; um módulo separado registra o handler para o mesmo `customId`.","blocks":[{"kind":"code","title":"Discord/Commands/Feedback.ts","code":"\nimport {\n  ActionRowBuilder,\n  ModalBuilder,\n  SlashCommandBuilder,\n  TextInputBuilder,\n  TextInputStyle\n} from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('feedback')\n    .setDescription('Abre o formulário de feedback.'),\n  execute: async interaction => {\n    const input = new TextInputBuilder()\n      .setCustomId('message')\n      .setLabel('Como podemos melhorar?')\n      .setStyle(TextInputStyle.Paragraph)\n      .setMinLength(10)\n      .setMaxLength(1000)\n      .setRequired(true)\n\n    const modal = new ModalBuilder()\n      .setCustomId('feedback-modal')\n      .setTitle('Enviar feedback')\n      .addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(input))\n\n    await interaction.interaction.showModal(modal)\n  }\n})\n          "},{"kind":"code","title":"Discord/Interactions/FeedbackModal.ts","code":"\nimport Client from '../../Shared/Settings/Client'\n\nClient.modal({\n  customId: 'feedback-modal',\n  execute: async interaction => {\n    const text = interaction.fields.getTextInputValue('message')\n\n    await saveFeedback({\n      authorId: interaction.user.id,\n      guildId: interaction.guildId,\n      text\n    })\n\n    await interaction.reply({\n      content: 'Obrigado pelo feedback!',\n      flags: 64\n    })\n  }\n})\n          "}]},{"id":"autocomplete","title":"Autocomplete","description":"Declare a opção com autocomplete no comando e responda com até 25 sugestões.","blocks":[{"kind":"code","title":"Discord/Commands/Search.ts","code":"\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('buscar')\n    .setDescription('Busca um projeto.')\n    .addStringOption(option => option\n      .setName('projeto')\n      .setDescription('Nome do projeto')\n      .setAutocomplete(true)\n      .setRequired(true)),\n  autocomplete: async interaction => {\n    const query = interaction.options.getFocused().toLowerCase()\n    const choices = projects\n      .filter(project => project.name.toLowerCase().includes(query))\n      .slice(0, 25)\n      .map(project => ({ name: project.name, value: project.id }))\n\n    await interaction.respond(choices)\n  },\n  execute: async interaction => {\n    const projectId = interaction.options.getString('projeto', true)\n    await interaction.reply('Projeto escolhido: ' + projectId)\n  }\n})\n          "},{"kind":"callout","tone":"warning","title":"Autocomplete precisa ser rápido","text":"Filtre dados em memória ou use consultas indexadas. O Discord espera a lista enquanto o usuário digita; uma busca lenta torna o comando frustrante.","icon":"!"}]},{"id":"fluxos-registrados","title":"Prompts e paginações registrados","description":"Depois de registrar uma definição uma vez, qualquer slash ou prefix pode iniciar uma instância isolada para o usuário.","blocks":[{"kind":"code","title":"Discord/Commands/Flows.ts","code":"\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('confirmar')\n    .setDescription('Abre um prompt.'),\n  execute: async interaction => {\n    await interaction.promptRequest('delete-prompt', { ephemeral: true })\n  }\n})\n\nClient.prefix({\n  data: new PrefixCommandBuilder().setName('confirmar'),\n  execute: async interaction => {\n    await interaction.promptRequest('delete-prompt', { ephemeral: true })\n  }\n})\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('catalogo')\n    .setDescription('Abre o catálogo paginado.'),\n  execute: async interaction => {\n    await interaction.paginatorRequest('catalog-pages', {\n      ephemeral: true,\n      pageIndex: 0\n    })\n  }\n})\n          "},{"kind":"text","text":"A criação das definições fica em `Discord/Interactions`. Consulte as páginas **Prompts** e **Paginação** para montar templates, callbacks e controles personalizados."}]}],"url":"https://ninenity.vercel.app/doc/interacoes","markdownUrl":"https://ninenity.vercel.app/doc/markdown/interacoes","markdown":"# Interações\n> Entenda respostas, modais, autocomplete e o contexto normalizado usado em todos os fluxos interativos.\n\n- Área: Base do Core\n- URL humana: https://ninenity.vercel.app/doc/interacoes\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/interacoes\n- Pacotes e conceitos: Replies, Modal, Autocomplete\n\n## O ciclo de uma resposta\n\nUma interação pode receber uma resposta inicial e depois ser editada ou acompanhada por mensagens adicionais.\n\n| Método | Quando usar |\n| --- | --- |\n| `reply()` | Primeira resposta imediata |\n| `deferReply()` | Reserva a resposta quando o trabalho demora |\n| `editReply()` | Finaliza ou atualiza a resposta reservada |\n| `followUp()` | Envia uma resposta adicional |\n| `deleteReply()` | Remove a resposta original |\n\n### resposta demorada\n\n```text\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('relatorio')\n    .setDescription('Gera um relatório detalhado.'),\n  execute: async interaction => {\n    await interaction.deferReply({ flags: 64 })\n\n    const report = await generateReport(interaction.guildId)\n\n    await interaction.editReply('Relatório pronto: ' + report.url)\n    await interaction.followUp({\n      content: 'A exportação expira em 24 horas.',\n      flags: 64\n    })\n  }\n})\n```\n\n\n## Contexto normalizado\n\nO Core fornece uma superfície comum e mantém a interação original em `interaction.interaction` quando você precisa estreitar o tipo.\n\n- **Identidade**: `user`, `member`, `guild`, `guildId`, `channel` e `channelId`.\n- **Resposta**: `reply`, `deferReply`, `editReply`, `followUp` e estado da resposta.\n- **Core**: `promptRequest`, `paginatorRequest`, configuração e contexto isolado.\n- **Discord.js**: A propriedade `interaction` preserva a interação nativa completa.\n\n### acesso à configuração da guild atual\n\n```text\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('canal-suporte')\n    .setDescription('Mostra o canal configurado.'),\n  execute: async interaction => {\n    const config = Client.appConfig()\n    const supportChannel = Client.getAppChannels().support\n\n    await interaction.reply({\n      content: 'Canal: ' + (supportChannel?.toString() ?? 'não configurado'),\n      flags: 64\n    })\n  }\n})\n```\n\n\n## Abrindo um modal\n\nO comando constrói o modal; um módulo separado registra o handler para o mesmo `customId`.\n\n### Discord/Commands/Feedback.ts\n\n```text\n\nimport {\n  ActionRowBuilder,\n  ModalBuilder,\n  SlashCommandBuilder,\n  TextInputBuilder,\n  TextInputStyle\n} from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('feedback')\n    .setDescription('Abre o formulário de feedback.'),\n  execute: async interaction => {\n    const input = new TextInputBuilder()\n      .setCustomId('message')\n      .setLabel('Como podemos melhorar?')\n      .setStyle(TextInputStyle.Paragraph)\n      .setMinLength(10)\n      .setMaxLength(1000)\n      .setRequired(true)\n\n    const modal = new ModalBuilder()\n      .setCustomId('feedback-modal')\n      .setTitle('Enviar feedback')\n      .addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(input))\n\n    await interaction.interaction.showModal(modal)\n  }\n})\n```\n\n\n### Discord/Interactions/FeedbackModal.ts\n\n```text\n\nimport Client from '../../Shared/Settings/Client'\n\nClient.modal({\n  customId: 'feedback-modal',\n  execute: async interaction => {\n    const text = interaction.fields.getTextInputValue('message')\n\n    await saveFeedback({\n      authorId: interaction.user.id,\n      guildId: interaction.guildId,\n      text\n    })\n\n    await interaction.reply({\n      content: 'Obrigado pelo feedback!',\n      flags: 64\n    })\n  }\n})\n```\n\n\n## Autocomplete\n\nDeclare a opção com autocomplete no comando e responda com até 25 sugestões.\n\n### Discord/Commands/Search.ts\n\n```text\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('buscar')\n    .setDescription('Busca um projeto.')\n    .addStringOption(option => option\n      .setName('projeto')\n      .setDescription('Nome do projeto')\n      .setAutocomplete(true)\n      .setRequired(true)),\n  autocomplete: async interaction => {\n    const query = interaction.options.getFocused().toLowerCase()\n    const choices = projects\n      .filter(project => project.name.toLowerCase().includes(query))\n      .slice(0, 25)\n      .map(project => ({ name: project.name, value: project.id }))\n\n    await interaction.respond(choices)\n  },\n  execute: async interaction => {\n    const projectId = interaction.options.getString('projeto', true)\n    await interaction.reply('Projeto escolhido: ' + projectId)\n  }\n})\n```\n\n\n> **Autocomplete precisa ser rápido:** Filtre dados em memória ou use consultas indexadas. O Discord espera a lista enquanto o usuário digita; uma busca lenta torna o comando frustrante.\n\n## Prompts e paginações registrados\n\nDepois de registrar uma definição uma vez, qualquer slash ou prefix pode iniciar uma instância isolada para o usuário.\n\n### Discord/Commands/Flows.ts\n\n```text\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('confirmar')\n    .setDescription('Abre um prompt.'),\n  execute: async interaction => {\n    await interaction.promptRequest('delete-prompt', { ephemeral: true })\n  }\n})\n\nClient.prefix({\n  data: new PrefixCommandBuilder().setName('confirmar'),\n  execute: async interaction => {\n    await interaction.promptRequest('delete-prompt', { ephemeral: true })\n  }\n})\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('catalogo')\n    .setDescription('Abre o catálogo paginado.'),\n  execute: async interaction => {\n    await interaction.paginatorRequest('catalog-pages', {\n      ephemeral: true,\n      pageIndex: 0\n    })\n  }\n})\n```\n\n\nA criação das definições fica em `Discord/Interactions`. Consulte as páginas **Prompts** e **Paginação** para montar templates, callbacks e controles personalizados.\n"},{"slug":"eventos","title":"Eventos","group":"Base do Core","eyebrow":"BASE DO CORE / EVENTOS","description":"Reaja ao ciclo de vida do Discord com módulos pequenos, tipos do Discord.js e contexto de guild preservado.","badges":["Client.on","Client.once","Events"],"sections":[{"id":"on-once","title":"Eventos contínuos e únicos","description":"Use `Client.on()` para todas as emissões e `Client.once()` quando o handler deve rodar uma única vez.","blocks":[{"kind":"code","title":"Discord/Events/Ready.ts","code":"\nimport { ActivityType, Events } from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.once(Events.ClientReady, {\n  execute: async readyClient => {\n    readyClient.user.setPresence({\n      activities: [\n        { name: 'Ninenity', type: ActivityType.Watching }\n      ],\n      status: 'online'\n    })\n\n    console.log('[Ready] ' + readyClient.user.tag + ' online')\n  }\n})\n          "},{"kind":"code","title":"Discord/Events/MessageCreate.ts","code":"\nimport { Events, type Message } from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.on(Events.MessageCreate, {\n  execute: async (message: Message) => {\n    if (message.author.bot) return\n    if (message.content.trim().toLowerCase() !== 'hello') return\n\n    await message.reply('Olá, ' + message.author.username + '!')\n  }\n})\n          "}]},{"id":"membros","title":"Entrada de membros","description":"O evento recebe os objetos nativos e pode usar recursos configurados para a guild atual.","blocks":[{"kind":"code","title":"Discord/Events/MemberJoin.ts","code":"\nimport { Events, type GuildMember } from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.on(Events.GuildMemberAdd, {\n  execute: async (member: GuildMember) => {\n    const channels = Client.getAppChannels()\n    const welcome = channels.welcome ?? member.guild.systemChannel\n\n    if (!welcome?.isTextBased()) return\n\n    await welcome.send(\n      'Boas-vindas, ' + member.toString() + '! Leia as regras para começar.'\n    )\n  }\n})\n          "},{"kind":"callout","tone":"info","title":"Contexto de guild","text":"Durante o evento, `Client.appConfig()` e os aliases de canais/cargos resolvem a guild do objeto emitido. Fora de qualquer contexto, informe o `guildId` explicitamente.","icon":"◎"}]},{"id":"erros","title":"Erros e avisos do Discord","description":"Centralize mensagens técnicas sem esconder o contexto da origem.","blocks":[{"kind":"code","title":"Discord/Events/Process.ts","code":"\nimport { Events } from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.on(Events.Warn, {\n  execute: async (message: string) => {\n    console.log('[Discord:warn] ' + message)\n  }\n})\n\nClient.on(Events.Error, {\n  execute: async (error: Error) => {\n    console.log('[Discord:error] ' + error.message)\n  }\n})\n          "},{"kind":"text","text":"Use `console.log()` para os logs da aplicação. Se quiser cor no terminal, aplique `Color()` somente ao texto; não introduza uma segunda abstração de logger no código do bot."}]},{"id":"boas-praticas-eventos","title":"Boas práticas","blocks":[{"kind":"list","items":["Retorne cedo para ignorar bots, DMs ou eventos que não interessam.","Não faça tarefas longas em série dentro de eventos muito frequentes como `MessageCreate`.","Extraia regras de negócio para `Utils` ou serviços e mantenha o handler como orquestrador.","Capture falhas esperadas de rede ao enviar mensagens; uma permissão removida não deve derrubar o processo.","Use um arquivo por evento ou por responsabilidade claramente relacionada."]}]}],"url":"https://ninenity.vercel.app/doc/eventos","markdownUrl":"https://ninenity.vercel.app/doc/markdown/eventos","markdown":"# Eventos\n> Reaja ao ciclo de vida do Discord com módulos pequenos, tipos do Discord.js e contexto de guild preservado.\n\n- Área: Base do Core\n- URL humana: https://ninenity.vercel.app/doc/eventos\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/eventos\n- Pacotes e conceitos: Client.on, Client.once, Events\n\n## Eventos contínuos e únicos\n\nUse `Client.on()` para todas as emissões e `Client.once()` quando o handler deve rodar uma única vez.\n\n### Discord/Events/Ready.ts\n\n```text\n\nimport { ActivityType, Events } from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.once(Events.ClientReady, {\n  execute: async readyClient => {\n    readyClient.user.setPresence({\n      activities: [\n        { name: 'Ninenity', type: ActivityType.Watching }\n      ],\n      status: 'online'\n    })\n\n    console.log('[Ready] ' + readyClient.user.tag + ' online')\n  }\n})\n```\n\n\n### Discord/Events/MessageCreate.ts\n\n```text\n\nimport { Events, type Message } from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.on(Events.MessageCreate, {\n  execute: async (message: Message) => {\n    if (message.author.bot) return\n    if (message.content.trim().toLowerCase() !== 'hello') return\n\n    await message.reply('Olá, ' + message.author.username + '!')\n  }\n})\n```\n\n\n## Entrada de membros\n\nO evento recebe os objetos nativos e pode usar recursos configurados para a guild atual.\n\n### Discord/Events/MemberJoin.ts\n\n```text\n\nimport { Events, type GuildMember } from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.on(Events.GuildMemberAdd, {\n  execute: async (member: GuildMember) => {\n    const channels = Client.getAppChannels()\n    const welcome = channels.welcome ?? member.guild.systemChannel\n\n    if (!welcome?.isTextBased()) return\n\n    await welcome.send(\n      'Boas-vindas, ' + member.toString() + '! Leia as regras para começar.'\n    )\n  }\n})\n```\n\n\n> **Contexto de guild:** Durante o evento, `Client.appConfig()` e os aliases de canais/cargos resolvem a guild do objeto emitido. Fora de qualquer contexto, informe o `guildId` explicitamente.\n\n## Erros e avisos do Discord\n\nCentralize mensagens técnicas sem esconder o contexto da origem.\n\n### Discord/Events/Process.ts\n\n```text\n\nimport { Events } from '@ninenity/core'\nimport Client from '../../Shared/Settings/Client'\n\nClient.on(Events.Warn, {\n  execute: async (message: string) => {\n    console.log('[Discord:warn] ' + message)\n  }\n})\n\nClient.on(Events.Error, {\n  execute: async (error: Error) => {\n    console.log('[Discord:error] ' + error.message)\n  }\n})\n```\n\n\nUse `console.log()` para os logs da aplicação. Se quiser cor no terminal, aplique `Color()` somente ao texto; não introduza uma segunda abstração de logger no código do bot.\n\n## Boas práticas\n\n- Retorne cedo para ignorar bots, DMs ou eventos que não interessam.\n- Não faça tarefas longas em série dentro de eventos muito frequentes como `MessageCreate`.\n- Extraia regras de negócio para `Utils` ou serviços e mantenha o handler como orquestrador.\n- Capture falhas esperadas de rede ao enviar mensagens; uma permissão removida não deve derrubar o processo.\n- Use um arquivo por evento ou por responsabilidade claramente relacionada.\n"},{"slug":"tasks","title":"Tasks","group":"Base do Core","eyebrow":"BASE DO CORE / TASKS","description":"Execute rotinas periódicas globais ou por guild sem misturar timers com os handlers do Discord.","badges":["Global","Guild","Controle manual"],"sections":[{"id":"quando-usar","title":"Quando usar uma Task","blocks":[{"kind":"cards","columns":3,"items":[{"icon":"↻","title":"Sincronização","text":"Atualizar cache, telemetria ou dados de uma API em intervalos previsíveis."},{"icon":"⌁","title":"Manutenção","text":"Limpar registros expirados, revisar configurações ou renovar estados."},{"icon":"◎","title":"Rotina por guild","text":"Executar a mesma regra com `guildId` e configuração isolados."}]},{"kind":"callout","tone":"warning","title":"Task não é um worker infinito","text":"A função `execute` deve terminar. O runtime agenda a próxima execução; não crie `while (true)` nem um segundo `setInterval` dentro dela.","icon":"!"}]},{"id":"task-global","title":"Task global","description":"Uma task global roda uma vez por intervalo para o Client inteiro.","blocks":[{"kind":"code","title":"Discord/Tasks/Telemetry.ts","code":"\nimport Client from '../../Shared/Settings/Client'\n\nClient.task({\n  id: 'global-telemetry',\n  timeout: '5m',\n  runOnStart: true,\n  scope: 'global',\n  execute: async client => {\n    console.log(\n      '[Task:telemetry] guilds=' + client.guilds.cache.size +\n      ' users=' + client.users.cache.size\n    )\n  }\n})\n          "},{"kind":"table","columns":["Campo","Função"],"rows":[["`id`","Identificador único usado também no controle manual"],["`timeout`","Intervalo em ms ou texto como `30s`, `5m`, `1h`"],["`runOnStart`","Executa logo no boot antes de esperar o primeiro intervalo"],["`scope`","`global` executa uma vez; `guild` executa para cada servidor"],["`execute`","Função assíncrona que recebe o Client e, em guild, o `guildId`"]]}]},{"id":"task-guild","title":"Task por guild","description":"O runtime chama a execução separadamente para cada guild e ativa o contexto usado por `appConfig()`.","blocks":[{"kind":"code","title":"Discord/Tasks/GuildMaintenance.ts","code":"\nimport Client from '../../Shared/Settings/Client'\n\nClient.task({\n  id: 'guild-maintenance',\n  timeout: '10m',\n  runOnStart: false,\n  scope: 'guild',\n  execute: async (client, guildId) => {\n    if (!guildId) return\n\n    const guild = client.guilds.cache.get(guildId)\n    const config = client.appConfig(guildId)\n    if (!guild || config.variables?.maintenanceEnabled === false) return\n\n    await removeExpiredRecords(guildId)\n    console.log('[Task:maintenance] guild=' + guild.name)\n  }\n})\n          "},{"kind":"callout","tone":"success","title":"Estado isolado","text":"Tasks de guild não compartilham o contexto de canais, cargos, configuração ou fila com outro servidor. Ainda assim, dados globais criados por você precisam ser indexados por `guildId`.","icon":"✓"}]},{"id":"controle-manual","title":"Iniciar, parar e executar manualmente","description":"Use os controles pelo `id` quando uma operação administrativa precisar alterar a rotina.","blocks":[{"kind":"code","title":"controle em comando administrativo","code":"\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('maintenance-run')\n    .setDescription('Executa a manutenção agora.'),\n  execute: async interaction => {\n    await interaction.deferReply({ flags: 64 })\n    await Client.runTask('guild-maintenance')\n    await interaction.editReply('Manutenção executada.')\n  }\n})\n\n// Pausa os próximos ciclos\nClient.stopTask('guild-maintenance')\n\n// Reativa o agendamento\nClient.startTask('guild-maintenance')\n          "},{"kind":"text","text":"`runTask()` dispara uma execução sem substituir o agendamento. `stopTask()` limpa o timer daquela task no Client atual; `startTask()` cria o ciclo novamente."}]},{"id":"concorrencia-task","title":"Evite sobreposição","description":"Se a execução pode durar mais que o intervalo, proteja a seção crítica com a fila do Core.","blocks":[{"kind":"code","title":"task com fila","code":"\nClient.task({\n  id: 'sync-catalog',\n  timeout: '1m',\n  scope: 'global',\n  execute: async () => {\n    await Client.queue('task:sync-catalog', async () => {\n      const items = await catalogApi.list()\n      await catalogRepository.replace(items)\n      console.log('[Task:catalog] items=' + items.length)\n    })\n  }\n})\n          "},{"kind":"list","items":["Escolha intervalos maiores que o tempo normal da operação.","Defina timeout nas requisições externas chamadas pela task.","Faça a rotina ser idempotente: repetir não deve duplicar dados.","Registre apenas início, resultado e falhas úteis; evite logs a cada item processado."]}]}],"url":"https://ninenity.vercel.app/doc/tasks","markdownUrl":"https://ninenity.vercel.app/doc/markdown/tasks","markdown":"# Tasks\n> Execute rotinas periódicas globais ou por guild sem misturar timers com os handlers do Discord.\n\n- Área: Base do Core\n- URL humana: https://ninenity.vercel.app/doc/tasks\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/tasks\n- Pacotes e conceitos: Global, Guild, Controle manual\n\n## Quando usar uma Task\n\n- **Sincronização**: Atualizar cache, telemetria ou dados de uma API em intervalos previsíveis.\n- **Manutenção**: Limpar registros expirados, revisar configurações ou renovar estados.\n- **Rotina por guild**: Executar a mesma regra com `guildId` e configuração isolados.\n\n> **Task não é um worker infinito:** A função `execute` deve terminar. O runtime agenda a próxima execução; não crie `while (true)` nem um segundo `setInterval` dentro dela.\n\n## Task global\n\nUma task global roda uma vez por intervalo para o Client inteiro.\n\n### Discord/Tasks/Telemetry.ts\n\n```text\n\nimport Client from '../../Shared/Settings/Client'\n\nClient.task({\n  id: 'global-telemetry',\n  timeout: '5m',\n  runOnStart: true,\n  scope: 'global',\n  execute: async client => {\n    console.log(\n      '[Task:telemetry] guilds=' + client.guilds.cache.size +\n      ' users=' + client.users.cache.size\n    )\n  }\n})\n```\n\n\n| Campo | Função |\n| --- | --- |\n| `id` | Identificador único usado também no controle manual |\n| `timeout` | Intervalo em ms ou texto como `30s`, `5m`, `1h` |\n| `runOnStart` | Executa logo no boot antes de esperar o primeiro intervalo |\n| `scope` | `global` executa uma vez; `guild` executa para cada servidor |\n| `execute` | Função assíncrona que recebe o Client e, em guild, o `guildId` |\n\n## Task por guild\n\nO runtime chama a execução separadamente para cada guild e ativa o contexto usado por `appConfig()`.\n\n### Discord/Tasks/GuildMaintenance.ts\n\n```text\n\nimport Client from '../../Shared/Settings/Client'\n\nClient.task({\n  id: 'guild-maintenance',\n  timeout: '10m',\n  runOnStart: false,\n  scope: 'guild',\n  execute: async (client, guildId) => {\n    if (!guildId) return\n\n    const guild = client.guilds.cache.get(guildId)\n    const config = client.appConfig(guildId)\n    if (!guild || config.variables?.maintenanceEnabled === false) return\n\n    await removeExpiredRecords(guildId)\n    console.log('[Task:maintenance] guild=' + guild.name)\n  }\n})\n```\n\n\n> **Estado isolado:** Tasks de guild não compartilham o contexto de canais, cargos, configuração ou fila com outro servidor. Ainda assim, dados globais criados por você precisam ser indexados por `guildId`.\n\n## Iniciar, parar e executar manualmente\n\nUse os controles pelo `id` quando uma operação administrativa precisar alterar a rotina.\n\n### controle em comando administrativo\n\n```text\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('maintenance-run')\n    .setDescription('Executa a manutenção agora.'),\n  execute: async interaction => {\n    await interaction.deferReply({ flags: 64 })\n    await Client.runTask('guild-maintenance')\n    await interaction.editReply('Manutenção executada.')\n  }\n})\n\n// Pausa os próximos ciclos\nClient.stopTask('guild-maintenance')\n\n// Reativa o agendamento\nClient.startTask('guild-maintenance')\n```\n\n\n`runTask()` dispara uma execução sem substituir o agendamento. `stopTask()` limpa o timer daquela task no Client atual; `startTask()` cria o ciclo novamente.\n\n## Evite sobreposição\n\nSe a execução pode durar mais que o intervalo, proteja a seção crítica com a fila do Core.\n\n### task com fila\n\n```text\n\nClient.task({\n  id: 'sync-catalog',\n  timeout: '1m',\n  scope: 'global',\n  execute: async () => {\n    await Client.queue('task:sync-catalog', async () => {\n      const items = await catalogApi.list()\n      await catalogRepository.replace(items)\n      console.log('[Task:catalog] items=' + items.length)\n    })\n  }\n})\n```\n\n\n- Escolha intervalos maiores que o tempo normal da operação.\n- Defina timeout nas requisições externas chamadas pela task.\n- Faça a rotina ser idempotente: repetir não deve duplicar dados.\n- Registre apenas início, resultado e falhas úteis; evite logs a cada item processado.\n"},{"slug":"component-builder","title":"ComponentBuilder","group":"Componentes","eyebrow":"COMPONENTES / VISÃO GERAL","description":"Componha mensagens Components V2 com builders tipados, templates reutilizáveis e integração direta com o runtime do Core.","badges":["@ninenity/componentbuilder","Components V2","Templates"],"sections":[{"id":"oque-e","title":"O que o ComponentBuilder resolve","description":"Ele representa a mensagem como uma árvore validável antes de converter o documento para o formato aceito pelo Discord.js.","blocks":[{"kind":"cards","columns":3,"items":[{"icon":"◇","title":"Composição","text":"Builders pequenos representam texto, seções, botões, selects, galerias e containers."},{"icon":"✓","title":"Validação","text":"Limites do Discord são verificados antes de a mensagem chegar à API."},{"icon":"↯","title":"Runtime","text":"Callbacks, prompts e paginações são conectados automaticamente ao Core."}]},{"kind":"text","text":"O fluxo possui quatro etapas: você monta builders, gera um `ComponentTemplate`, converte o documento com `toDiscordMessagePayload()` e envia pela interação do Discord."},{"kind":"code","title":"fluxo mínimo","code":"\nconst template = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder().addTextDisplayComponents(\n      new TextDisplayBuilder().setContent('Olá, Components V2!')\n    )\n  )\n  .template()\n\nawait interaction.reply({\n  ...toDiscordMessagePayload(template.document()),\n  flags: MessageFlags.IsComponentsV2\n})\n          "}]},{"id":"camadas","title":"As quatro camadas","blocks":[{"kind":"table","columns":["Camada","Papel","Exemplos"],"rows":[["Mensagem","Payload completo","`MessageBuilder`"],["Layout","Organiza a hierarquia visual","`ContainerBuilder`, `SectionBuilder`, `ActionRowBuilder`"],["Conteúdo","Mostra informação","`TextDisplayBuilder`, `MediaGalleryBuilder`, `FileBuilder`"],["Interação","Recebe ações do usuário","`ButtonBuilder` e builders de select"]]},{"kind":"callout","tone":"info","title":"Builder não é a mensagem enviada","text":"O builder é mutável enquanto você monta. `build()` gera um documento simples; `template()` gera um objeto reutilizável capaz de renderizar variáveis.","icon":"i"}]},{"id":"imports","title":"Imports recomendados","description":"Importe Discord.js pelo Core e os builders visuais pelo ComponentBuilder.","blocks":[{"kind":"code","title":"imports.ts","code":"\nimport {\n  MessageFlags,\n  SlashCommandBuilder\n} from '@ninenity/core'\n\nimport {\n  ActionRowBuilder,\n  ButtonBuilder,\n  ButtonStyle,\n  ContainerBuilder,\n  MessageBuilder,\n  SectionBuilder,\n  TextDisplayBuilder,\n  toDiscordMessagePayload,\n  type MessageActionRowComponentBuilder\n} from '@ninenity/componentbuilder'\n          "},{"kind":"list","items":["Use `MessageFlags.IsComponentsV2` ao enviar uma árvore Components V2.","Adicione `MessageFlags.Ephemeral` quando a resposta só deve aparecer para o autor.","Use o tipo `MessageActionRowComponentBuilder` no `ActionRowBuilder` quando misturar builders interativos.","Não importe APIs internas de `src`; use apenas os exports do pacote."]}]},{"id":"document-template","title":"Documento, template e payload","description":"Cada representação existe para um momento diferente do fluxo.","blocks":[{"kind":"code","title":"três representações","code":"\nconst builder = new MessageBuilder().setContent('Status: pronto')\n\n// Objeto serializável do ComponentBuilder\nconst document = builder.build()\n\n// Reutilizável, renderizável e exportável\nconst template = builder.template({ name: 'status-card' })\n\n// Payload final para interaction.reply/update\nconst payload = toDiscordMessagePayload(template.document())\n          "},{"kind":"table","columns":["Método","Retorna","Use quando"],"rows":[["`build()`","`MessageDocument`","Precisa inspecionar ou serializar a árvore"],["`template()`","`ComponentTemplate`","Vai reutilizar, renderizar tokens ou registrar páginas"],["`template.document()`","`MessageDocument`","Vai converter e enviar ao Discord"],["`toDiscordMessagePayload()`","Payload Discord.js","Etapa imediatamente anterior a `reply()` ou `update()`"]]}]},{"id":"limites","title":"Limites estruturais","description":"O Builder Studio e a biblioteca aplicam os limites mais importantes antes do envio.","blocks":[{"kind":"table","columns":["Estrutura","Regra"],"rows":[["Mensagem","Até 40 componentes na árvore"],["Container","Até 10 filhos e sem containers aninhados"],["Action row","Até 5 botões ou um único select"],["Section","Acessório deve ser botão ou thumbnail"],["Galeria","Entre 1 e 10 imagens"],["Interativos","Cada `customId` deve ser único na mensagem"]]},{"kind":"callout","tone":"warning","title":"Valide no ponto de criação","text":"Se dados externos geram componentes, limite a coleção antes de chamar o builder. Não espere o Discord rejeitar um payload grande para descobrir o problema.","icon":"!"}]},{"id":"proximos-passos","title":"Escolha o próximo tópico","blocks":[{"kind":"cards","items":[{"icon":"▱","title":"Mensagens V2","text":"Layouts completos com seções, mídia, botões e selects.","href":"/doc/mensagens-v2"},{"icon":"↯","title":"Callbacks","text":"Como tratar cliques e seleções sem registradores duplicados.","href":"/doc/callbacks"},{"icon":"?","title":"Prompts","text":"Fluxos de confirmação com páginas de resultado.","href":"/doc/prompts"},{"icon":"↔","title":"Paginação","text":"Páginas estáticas, dinâmicas e controles personalizados.","href":"/doc/paginacao"}]}]}],"url":"https://ninenity.vercel.app/doc/component-builder","markdownUrl":"https://ninenity.vercel.app/doc/markdown/component-builder","markdown":"# ComponentBuilder\n> Componha mensagens Components V2 com builders tipados, templates reutilizáveis e integração direta com o runtime do Core.\n\n- Área: Componentes\n- URL humana: https://ninenity.vercel.app/doc/component-builder\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/component-builder\n- Pacotes e conceitos: @ninenity/componentbuilder, Components V2, Templates\n\n## O que o ComponentBuilder resolve\n\nEle representa a mensagem como uma árvore validável antes de converter o documento para o formato aceito pelo Discord.js.\n\n- **Composição**: Builders pequenos representam texto, seções, botões, selects, galerias e containers.\n- **Validação**: Limites do Discord são verificados antes de a mensagem chegar à API.\n- **Runtime**: Callbacks, prompts e paginações são conectados automaticamente ao Core.\n\nO fluxo possui quatro etapas: você monta builders, gera um `ComponentTemplate`, converte o documento com `toDiscordMessagePayload()` e envia pela interação do Discord.\n\n### fluxo mínimo\n\n```text\n\nconst template = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder().addTextDisplayComponents(\n      new TextDisplayBuilder().setContent('Olá, Components V2!')\n    )\n  )\n  .template()\n\nawait interaction.reply({\n  ...toDiscordMessagePayload(template.document()),\n  flags: MessageFlags.IsComponentsV2\n})\n```\n\n\n## As quatro camadas\n\n| Camada | Papel | Exemplos |\n| --- | --- | --- |\n| Mensagem | Payload completo | `MessageBuilder` |\n| Layout | Organiza a hierarquia visual | `ContainerBuilder`, `SectionBuilder`, `ActionRowBuilder` |\n| Conteúdo | Mostra informação | `TextDisplayBuilder`, `MediaGalleryBuilder`, `FileBuilder` |\n| Interação | Recebe ações do usuário | `ButtonBuilder` e builders de select |\n\n> **Builder não é a mensagem enviada:** O builder é mutável enquanto você monta. `build()` gera um documento simples; `template()` gera um objeto reutilizável capaz de renderizar variáveis.\n\n## Imports recomendados\n\nImporte Discord.js pelo Core e os builders visuais pelo ComponentBuilder.\n\n### imports.ts\n\n```text\n\nimport {\n  MessageFlags,\n  SlashCommandBuilder\n} from '@ninenity/core'\n\nimport {\n  ActionRowBuilder,\n  ButtonBuilder,\n  ButtonStyle,\n  ContainerBuilder,\n  MessageBuilder,\n  SectionBuilder,\n  TextDisplayBuilder,\n  toDiscordMessagePayload,\n  type MessageActionRowComponentBuilder\n} from '@ninenity/componentbuilder'\n```\n\n\n- Use `MessageFlags.IsComponentsV2` ao enviar uma árvore Components V2.\n- Adicione `MessageFlags.Ephemeral` quando a resposta só deve aparecer para o autor.\n- Use o tipo `MessageActionRowComponentBuilder` no `ActionRowBuilder` quando misturar builders interativos.\n- Não importe APIs internas de `src`; use apenas os exports do pacote.\n\n## Documento, template e payload\n\nCada representação existe para um momento diferente do fluxo.\n\n### três representações\n\n```text\n\nconst builder = new MessageBuilder().setContent('Status: pronto')\n\n// Objeto serializável do ComponentBuilder\nconst document = builder.build()\n\n// Reutilizável, renderizável e exportável\nconst template = builder.template({ name: 'status-card' })\n\n// Payload final para interaction.reply/update\nconst payload = toDiscordMessagePayload(template.document())\n```\n\n\n| Método | Retorna | Use quando |\n| --- | --- | --- |\n| `build()` | `MessageDocument` | Precisa inspecionar ou serializar a árvore |\n| `template()` | `ComponentTemplate` | Vai reutilizar, renderizar tokens ou registrar páginas |\n| `template.document()` | `MessageDocument` | Vai converter e enviar ao Discord |\n| `toDiscordMessagePayload()` | Payload Discord.js | Etapa imediatamente anterior a `reply()` ou `update()` |\n\n## Limites estruturais\n\nO Builder Studio e a biblioteca aplicam os limites mais importantes antes do envio.\n\n| Estrutura | Regra |\n| --- | --- |\n| Mensagem | Até 40 componentes na árvore |\n| Container | Até 10 filhos e sem containers aninhados |\n| Action row | Até 5 botões ou um único select |\n| Section | Acessório deve ser botão ou thumbnail |\n| Galeria | Entre 1 e 10 imagens |\n| Interativos | Cada `customId` deve ser único na mensagem |\n\n> **Valide no ponto de criação:** Se dados externos geram componentes, limite a coleção antes de chamar o builder. Não espere o Discord rejeitar um payload grande para descobrir o problema.\n\n## Escolha o próximo tópico\n\n- **Mensagens V2**: Layouts completos com seções, mídia, botões e selects. ([Abrir tópico](https://ninenity.vercel.app/doc/mensagens-v2))\n- **Callbacks**: Como tratar cliques e seleções sem registradores duplicados. ([Abrir tópico](https://ninenity.vercel.app/doc/callbacks))\n- **Prompts**: Fluxos de confirmação com páginas de resultado. ([Abrir tópico](https://ninenity.vercel.app/doc/prompts))\n- **Paginação**: Páginas estáticas, dinâmicas e controles personalizados. ([Abrir tópico](https://ninenity.vercel.app/doc/paginacao))\n"},{"slug":"mensagens-v2","title":"Mensagens Components V2","shortTitle":"Mensagens V2","group":"Componentes","eyebrow":"COMPONENTES / MENSAGENS V2","description":"Aprenda cada peça visual e combine containers, seções, mídia, action rows e selects em mensagens legíveis.","badges":["Container","Section","Gallery","Select"],"sections":[{"id":"mensagem-basica","title":"Mensagem básica com container","description":"O container cria um bloco visual e pode receber cor de destaque, texto e divisores.","blocks":[{"kind":"code","title":"status-card.ts","code":"\nimport {\n  ContainerBuilder,\n  MessageBuilder,\n  SeparatorBuilder,\n  TextDisplayBuilder\n} from '@ninenity/componentbuilder'\n\nexport const statusCard = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder()\n      .setAccentColor(0xa571f4)\n      .addTextDisplayComponents(\n        new TextDisplayBuilder().setContent('## Status do serviço'),\n        new TextDisplayBuilder().setContent('Todos os sistemas estão operacionais.')\n      )\n      .addSeparatorComponents(\n        new SeparatorBuilder().setDivider(true).setSpacing('small')\n      )\n      .addTextDisplayComponents(\n        new TextDisplayBuilder().setContent('- API: online\n- Bot: online\n- Banco: online')\n      )\n  )\n  .template()\n          "},{"kind":"text","text":"O conteúdo do `TextDisplayBuilder` aceita markdown suportado pelo Discord. Prefira blocos curtos e uma hierarquia clara em vez de uma única parede de texto."}]},{"id":"enviar","title":"Enviar pelo Core","description":"Converta o documento e combine as flags de Components V2 e resposta efêmera quando necessário.","blocks":[{"kind":"code","title":"Discord/Commands/Status.ts","code":"\nimport { MessageFlags, SlashCommandBuilder } from '@ninenity/core'\nimport { toDiscordMessagePayload } from '@ninenity/componentbuilder'\nimport { statusCard } from '../Interactions/StatusCard'\nimport Client from '../../Shared/Settings/Client'\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('status')\n    .setDescription('Mostra o estado dos serviços.'),\n  execute: async interaction => {\n    await interaction.reply({\n      ...toDiscordMessagePayload(statusCard.document()),\n      flags: MessageFlags.IsComponentsV2 | MessageFlags.Ephemeral\n    })\n  }\n})\n          "},{"kind":"callout","tone":"warning","title":"Não misture content com Components V2 sem validar","text":"Monte todo o conteúdo visual nos componentes. O payload V2 possui regras diferentes das mensagens clássicas e o conversor já prepara a estrutura correta.","icon":"!"}]},{"id":"section","title":"Seção com thumbnail ou botão","description":"Uma `SectionBuilder` combina texto com exatamente um acessório lateral.","blocks":[{"kind":"code","title":"project-section.ts","code":"\nconst withThumbnail = new SectionBuilder()\n  .addTextDisplayComponents(\n    new TextDisplayBuilder().setContent(\n      '### Builder Studio\nCrie e exporte interfaces Components V2.'\n    )\n  )\n  .setThumbnailAccessory(\n    new ThumbnailBuilder()\n      .setURL('https://cdn.example.com/builder.png')\n      .setDescription('Logo do Builder Studio')\n  )\n\nconst withButton = new SectionBuilder()\n  .setContent('### Documentação\nVeja todos os exemplos do componente.')\n  .setButtonAccessory(\n    new ButtonBuilder()\n      .setStyle(ButtonStyle.Link)\n      .setLabel('Abrir docs')\n      .setURL('https://ninenity.com/doc')\n  )\n\nconst template = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder().addSectionComponents(withThumbnail, withButton)\n  )\n  .template()\n          "},{"kind":"text","text":"Botões de link não possuem `customId` nem callback. Botões de ação precisam de `customId`, estilo diferente de `Link` e podem usar `.setCallback()`."}]},{"id":"galeria-arquivo","title":"Galeria e arquivo","description":"Use galeria para mídia visual e arquivo quando o anexo faz parte da composição.","blocks":[{"kind":"code","title":"media-card.ts","code":"\nconst gallery = new MediaGalleryBuilder()\n  .addItems(\n    item => item\n      .setURL('https://cdn.example.com/dashboard.png')\n      .setDescription('Dashboard do projeto'),\n    item => item\n      .setURL('https://cdn.example.com/components.png')\n      .setDescription('Componentes no Discord')\n  )\n\nconst template = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder()\n      .addTextDisplayComponents(\n        new TextDisplayBuilder().setContent('## Visão do projeto')\n      )\n      .addMediaGalleryComponents(gallery)\n      .addFileComponents(\n        new FileBuilder().setURL('attachment://relatorio.pdf')\n      )\n  )\n  .template()\n          "},{"kind":"list","items":["A galeria aceita no máximo 10 itens.","Use `setSpoiler(true)` em uma imagem ou arquivo que não deve aparecer imediatamente.","A URL `attachment://nome.ext` precisa corresponder a um arquivo realmente enviado no payload.","Sempre forneça descrição útil para imagens importantes."]}]},{"id":"botoes","title":"Linha de botões","description":"Uma action row aceita até cinco botões. Use estilos para significado, não apenas decoração.","blocks":[{"kind":"code","title":"actions.ts","code":"\nconst actions = new ActionRowBuilder<MessageActionRowComponentBuilder>()\n  .addComponents(\n    new ButtonBuilder()\n      .setCustomId('project:approve')\n      .setLabel('Aprovar')\n      .setStyle(ButtonStyle.Success),\n    new ButtonBuilder()\n      .setCustomId('project:edit')\n      .setLabel('Editar')\n      .setStyle(ButtonStyle.Secondary),\n    new ButtonBuilder()\n      .setCustomId('project:delete')\n      .setLabel('Excluir')\n      .setStyle(ButtonStyle.Danger),\n    new ButtonBuilder()\n      .setLabel('Ver no HUB')\n      .setStyle(ButtonStyle.Link)\n      .setURL('https://hub.ninenity.com')\n  )\n\nconst message = new MessageBuilder()\n  .addComponents(new ContainerBuilder().addActionRowComponents(actions))\n          "},{"kind":"table","columns":["Estilo","Use para"],"rows":[["`Primary`","A ação principal daquela etapa"],["`Secondary`","Ações neutras, navegação e alternativas"],["`Success`","Confirmar, concluir ou ativar"],["`Danger`","Excluir, revogar ou outra ação destrutiva"],["`Link`","Abrir URL; não recebe callback"]]}]},{"id":"selects","title":"Menus de seleção","description":"String select define opções; selects de usuário, cargo, canal e menção usam os seletores nativos do Discord.","blocks":[{"kind":"code","title":"string-select.ts","code":"\nconst projectSelect = new StringSelectMenuBuilder()\n  .setCustomId('project:environment')\n  .setPlaceholder('Selecione um ambiente')\n  .setMinValues(1)\n  .setMaxValues(1)\n  .addOptions(\n    option => option\n      .setLabel('Produção')\n      .setValue('production')\n      .setDescription('Ambiente público'),\n    option => option\n      .setLabel('Desenvolvimento')\n      .setValue('development')\n      .setDescription('Ambiente de testes')\n  )\n\nconst row = new ActionRowBuilder<MessageActionRowComponentBuilder>()\n  .addComponents(projectSelect)\n          "},{"kind":"code","title":"seletores nativos","code":"\nconst userRow = new ActionRowBuilder<MessageActionRowComponentBuilder>()\n  .addComponents(\n    new UserSelectMenuBuilder()\n      .setCustomId('team:members')\n      .setPlaceholder('Selecione até 3 membros')\n      .setMinValues(1)\n      .setMaxValues(3)\n  )\n\nconst roleRow = new ActionRowBuilder<MessageActionRowComponentBuilder>()\n  .addComponents(\n    new RoleSelectMenuBuilder()\n      .setCustomId('team:role')\n      .setPlaceholder('Selecione o cargo da equipe')\n  )\n          "}]},{"id":"templates","title":"Variáveis de template","description":"Tokens deixam o mesmo layout renderizar dados diferentes sem reconstruir toda a árvore.","blocks":[{"kind":"code","title":"member-template.ts","code":"\nconst memberTemplate = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder().addTextDisplayComponents(\n      new TextDisplayBuilder().setContent(\n        '## Olá, ${user.name}!\nSeu plano atual é **${account.plan}**.'\n      )\n    )\n  )\n  .template()\n\nconst rendered = memberTemplate.render({\n  user: { name: interaction.user.displayName },\n  account: { plan: 'Pro' }\n})\n\nawait interaction.reply({\n  ...toDiscordMessagePayload(rendered.document()),\n  flags: MessageFlags.IsComponentsV2\n})\n          "},{"kind":"callout","tone":"info","title":"Tokens de paginação","text":"O `Paginator` hidrata automaticamente `pages.current`, `pages.total`, `pages.hasNext` e `pages.hasBack`. Você não precisa calcular esses valores em páginas estáticas.","icon":"↔"}]}],"url":"https://ninenity.vercel.app/doc/mensagens-v2","markdownUrl":"https://ninenity.vercel.app/doc/markdown/mensagens-v2","markdown":"# Mensagens Components V2\n> Aprenda cada peça visual e combine containers, seções, mídia, action rows e selects em mensagens legíveis.\n\n- Área: Componentes\n- URL humana: https://ninenity.vercel.app/doc/mensagens-v2\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/mensagens-v2\n- Pacotes e conceitos: Container, Section, Gallery, Select\n\n## Mensagem básica com container\n\nO container cria um bloco visual e pode receber cor de destaque, texto e divisores.\n\n### status-card.ts\n\n```text\n\nimport {\n  ContainerBuilder,\n  MessageBuilder,\n  SeparatorBuilder,\n  TextDisplayBuilder\n} from '@ninenity/componentbuilder'\n\nexport const statusCard = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder()\n      .setAccentColor(0xa571f4)\n      .addTextDisplayComponents(\n        new TextDisplayBuilder().setContent('## Status do serviço'),\n        new TextDisplayBuilder().setContent('Todos os sistemas estão operacionais.')\n      )\n      .addSeparatorComponents(\n        new SeparatorBuilder().setDivider(true).setSpacing('small')\n      )\n      .addTextDisplayComponents(\n        new TextDisplayBuilder().setContent('- API: online\n- Bot: online\n- Banco: online')\n      )\n  )\n  .template()\n```\n\n\nO conteúdo do `TextDisplayBuilder` aceita markdown suportado pelo Discord. Prefira blocos curtos e uma hierarquia clara em vez de uma única parede de texto.\n\n## Enviar pelo Core\n\nConverta o documento e combine as flags de Components V2 e resposta efêmera quando necessário.\n\n### Discord/Commands/Status.ts\n\n```text\n\nimport { MessageFlags, SlashCommandBuilder } from '@ninenity/core'\nimport { toDiscordMessagePayload } from '@ninenity/componentbuilder'\nimport { statusCard } from '../Interactions/StatusCard'\nimport Client from '../../Shared/Settings/Client'\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('status')\n    .setDescription('Mostra o estado dos serviços.'),\n  execute: async interaction => {\n    await interaction.reply({\n      ...toDiscordMessagePayload(statusCard.document()),\n      flags: MessageFlags.IsComponentsV2 | MessageFlags.Ephemeral\n    })\n  }\n})\n```\n\n\n> **Não misture content com Components V2 sem validar:** Monte todo o conteúdo visual nos componentes. O payload V2 possui regras diferentes das mensagens clássicas e o conversor já prepara a estrutura correta.\n\n## Seção com thumbnail ou botão\n\nUma `SectionBuilder` combina texto com exatamente um acessório lateral.\n\n### project-section.ts\n\n```text\n\nconst withThumbnail = new SectionBuilder()\n  .addTextDisplayComponents(\n    new TextDisplayBuilder().setContent(\n      '### Builder Studio\nCrie e exporte interfaces Components V2.'\n    )\n  )\n  .setThumbnailAccessory(\n    new ThumbnailBuilder()\n      .setURL('https://cdn.example.com/builder.png')\n      .setDescription('Logo do Builder Studio')\n  )\n\nconst withButton = new SectionBuilder()\n  .setContent('### Documentação\nVeja todos os exemplos do componente.')\n  .setButtonAccessory(\n    new ButtonBuilder()\n      .setStyle(ButtonStyle.Link)\n      .setLabel('Abrir docs')\n      .setURL('https://ninenity.com/doc')\n  )\n\nconst template = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder().addSectionComponents(withThumbnail, withButton)\n  )\n  .template()\n```\n\n\nBotões de link não possuem `customId` nem callback. Botões de ação precisam de `customId`, estilo diferente de `Link` e podem usar `.setCallback()`.\n\n## Galeria e arquivo\n\nUse galeria para mídia visual e arquivo quando o anexo faz parte da composição.\n\n### media-card.ts\n\n```text\n\nconst gallery = new MediaGalleryBuilder()\n  .addItems(\n    item => item\n      .setURL('https://cdn.example.com/dashboard.png')\n      .setDescription('Dashboard do projeto'),\n    item => item\n      .setURL('https://cdn.example.com/components.png')\n      .setDescription('Componentes no Discord')\n  )\n\nconst template = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder()\n      .addTextDisplayComponents(\n        new TextDisplayBuilder().setContent('## Visão do projeto')\n      )\n      .addMediaGalleryComponents(gallery)\n      .addFileComponents(\n        new FileBuilder().setURL('attachment://relatorio.pdf')\n      )\n  )\n  .template()\n```\n\n\n- A galeria aceita no máximo 10 itens.\n- Use `setSpoiler(true)` em uma imagem ou arquivo que não deve aparecer imediatamente.\n- A URL `attachment://nome.ext` precisa corresponder a um arquivo realmente enviado no payload.\n- Sempre forneça descrição útil para imagens importantes.\n\n## Linha de botões\n\nUma action row aceita até cinco botões. Use estilos para significado, não apenas decoração.\n\n### actions.ts\n\n```text\n\nconst actions = new ActionRowBuilder<MessageActionRowComponentBuilder>()\n  .addComponents(\n    new ButtonBuilder()\n      .setCustomId('project:approve')\n      .setLabel('Aprovar')\n      .setStyle(ButtonStyle.Success),\n    new ButtonBuilder()\n      .setCustomId('project:edit')\n      .setLabel('Editar')\n      .setStyle(ButtonStyle.Secondary),\n    new ButtonBuilder()\n      .setCustomId('project:delete')\n      .setLabel('Excluir')\n      .setStyle(ButtonStyle.Danger),\n    new ButtonBuilder()\n      .setLabel('Ver no HUB')\n      .setStyle(ButtonStyle.Link)\n      .setURL('https://hub.ninenity.com')\n  )\n\nconst message = new MessageBuilder()\n  .addComponents(new ContainerBuilder().addActionRowComponents(actions))\n```\n\n\n| Estilo | Use para |\n| --- | --- |\n| `Primary` | A ação principal daquela etapa |\n| `Secondary` | Ações neutras, navegação e alternativas |\n| `Success` | Confirmar, concluir ou ativar |\n| `Danger` | Excluir, revogar ou outra ação destrutiva |\n| `Link` | Abrir URL; não recebe callback |\n\n## Menus de seleção\n\nString select define opções; selects de usuário, cargo, canal e menção usam os seletores nativos do Discord.\n\n### string-select.ts\n\n```text\n\nconst projectSelect = new StringSelectMenuBuilder()\n  .setCustomId('project:environment')\n  .setPlaceholder('Selecione um ambiente')\n  .setMinValues(1)\n  .setMaxValues(1)\n  .addOptions(\n    option => option\n      .setLabel('Produção')\n      .setValue('production')\n      .setDescription('Ambiente público'),\n    option => option\n      .setLabel('Desenvolvimento')\n      .setValue('development')\n      .setDescription('Ambiente de testes')\n  )\n\nconst row = new ActionRowBuilder<MessageActionRowComponentBuilder>()\n  .addComponents(projectSelect)\n```\n\n\n### seletores nativos\n\n```text\n\nconst userRow = new ActionRowBuilder<MessageActionRowComponentBuilder>()\n  .addComponents(\n    new UserSelectMenuBuilder()\n      .setCustomId('team:members')\n      .setPlaceholder('Selecione até 3 membros')\n      .setMinValues(1)\n      .setMaxValues(3)\n  )\n\nconst roleRow = new ActionRowBuilder<MessageActionRowComponentBuilder>()\n  .addComponents(\n    new RoleSelectMenuBuilder()\n      .setCustomId('team:role')\n      .setPlaceholder('Selecione o cargo da equipe')\n  )\n```\n\n\n## Variáveis de template\n\nTokens deixam o mesmo layout renderizar dados diferentes sem reconstruir toda a árvore.\n\n### member-template.ts\n\n```text\n\nconst memberTemplate = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder().addTextDisplayComponents(\n      new TextDisplayBuilder().setContent(\n        '## Olá, ${user.name}!\nSeu plano atual é **${account.plan}**.'\n      )\n    )\n  )\n  .template()\n\nconst rendered = memberTemplate.render({\n  user: { name: interaction.user.displayName },\n  account: { plan: 'Pro' }\n})\n\nawait interaction.reply({\n  ...toDiscordMessagePayload(rendered.document()),\n  flags: MessageFlags.IsComponentsV2\n})\n```\n\n\n> **Tokens de paginação:** O `Paginator` hidrata automaticamente `pages.current`, `pages.total`, `pages.hasNext` e `pages.hasBack`. Você não precisa calcular esses valores em páginas estáticas.\n"},{"slug":"callbacks","title":"Callbacks de componentes","shortTitle":"Callbacks","group":"Componentes","eyebrow":"COMPONENTES / CALLBACKS","description":"Trate botões e selects junto da definição visual, com IDs tipados e contexto de interação entregue pelo Core.","badges":["setCallback","customId tipado","Auto defer"],"sections":[{"id":"como-funciona","title":"Como o callback chega ao Core","blocks":[{"kind":"steps","items":[{"title":"O builder registra","text":"Ao combinar `.setCustomId()` e `.setCallback()`, o ComponentBuilder guarda a função somente em memória."},{"title":"O Discord envia o clique","text":"A interação chega ao `InteractionCreate` com o mesmo `customId`."},{"title":"O Core resolve","text":"O runtime encontra o callback, valida o contexto e entrega `interaction`, `id` e `context`."},{"title":"A resposta é concluída","text":"Se o callback não responder, o Core chama `deferUpdate()` para evitar o estado de falha no componente."}]},{"kind":"callout","tone":"warning","title":"Callbacks não são serializados","text":"Tokens e JSON preservam o layout, mas nunca código executável. Depois de importar um template, conecte os callbacks no código do bot ou use a exportação TypeScript do Builder Studio.","icon":"!"}]},{"id":"botao","title":"Callback de botão","description":"Defina o `customId` antes ou depois do callback; o registro acontece quando ambos estiverem presentes.","blocks":[{"kind":"code","title":"approve-button.ts","code":"\nconst approveButton = new ButtonBuilder()\n  .setCustomId('project:approve')\n  .setLabel('Aprovar projeto')\n  .setStyle(ButtonStyle.Success)\n  .setCallback(async (interaction, id, context) => {\n    await approveProject({\n      action: String(id),\n      userId: interaction.user.id,\n      guildId: interaction.guildId\n    })\n\n    await interaction.update({\n      content: 'Projeto aprovado com sucesso.',\n      components: []\n    })\n\n    console.log('[Component] customId=' + context.customId)\n  })\n          "},{"kind":"text","text":"O primeiro argumento é a interação real. O segundo é a parte lógica extraída do `customId`. O terceiro contém o ID completo, valores de select e o tipo do componente."}]},{"id":"id-tipado","title":"IDs segmentados e inferência","description":"O separador `:` transforma segmentos nomeados em um objeto tipado no callback.","blocks":[{"kind":"code","title":"typed-id.ts","code":"\nnew ButtonBuilder()\n  .setCustomId('project:42:archive')\n  .setLabel('Arquivar')\n  .setStyle(ButtonStyle.Secondary)\n  .setCallback(async (interaction, id) => {\n    // Para IDs segmentados, o editor infere as partes disponíveis.\n    console.log('[Project] callback=' + JSON.stringify(id))\n    await interaction.deferUpdate()\n  })\n          "},{"kind":"callout","tone":"info","title":"Escolha IDs estáveis","text":"Use IDs curtos que expressem domínio e ação, como `ticket:close` ou `profile:edit`. Dados sensíveis e payloads grandes pertencem ao banco, não ao `customId`.","icon":"i"}]},{"id":"select-callback","title":"Callback de select","description":"Os valores selecionados estão em `interaction.values` e também no `context` normalizado.","blocks":[{"kind":"code","title":"environment-select.ts","code":"\nconst environmentSelect = new StringSelectMenuBuilder()\n  .setCustomId('project:environment')\n  .setPlaceholder('Escolha o ambiente')\n  .addOptions(\n    option => option.setLabel('Produção').setValue('production'),\n    option => option.setLabel('Desenvolvimento').setValue('development')\n  )\n  .setCallback(async (interaction, _id, context) => {\n    const environment = context.value\n    if (!environment) return\n\n    await projectRepository.setEnvironment(\n      interaction.guildId,\n      environment\n    )\n\n    await interaction.update({\n      content: 'Ambiente alterado para ' + environment,\n      components: []\n    })\n  })\n          "},{"kind":"code","title":"seleção múltipla","code":"\nnew UserSelectMenuBuilder()\n  .setCustomId('team:members')\n  .setMinValues(1)\n  .setMaxValues(3)\n  .setCallback(async (interaction, _id, context) => {\n    const userIds = context.values\n    await teamRepository.replaceMembers(interaction.guildId, userIds)\n    await interaction.reply({\n      content: userIds.length + ' membros selecionados.',\n      flags: 64\n    })\n  })\n          "}]},{"id":"respostas","title":"Reply, update ou deferUpdate?","blocks":[{"kind":"table","columns":["Método","Resultado"],"rows":[["`interaction.update()`","Substitui a mensagem que contém o componente"],["`interaction.reply()`","Cria uma resposta separada ao clique"],["`interaction.deferUpdate()`","Confirma o clique sem mudar a mensagem"],["Nenhum","O Core faz `deferUpdate()` automaticamente"]]},{"kind":"code","title":"ação demorada","code":"\n.setCallback(async interaction => {\n  await interaction.deferReply({ flags: 64 })\n\n  const result = await runLongOperation()\n\n  await interaction.editReply(\n    result.ok ? 'Operação concluída.' : 'Não foi possível concluir.'\n  )\n})\n          "}]},{"id":"seguranca","title":"Autorização continua sendo sua regra","description":"O runtime resolve o callback, mas a permissão de negócio precisa ser verificada no handler.","blocks":[{"kind":"code","title":"callback protegido","code":"\n.setCallback(async interaction => {\n  const member = interaction.member\n  const canManage = member?.permissions?.has('ManageGuild')\n\n  if (!canManage) {\n    await interaction.reply({\n      content: 'Você não pode executar esta ação.',\n      flags: 64\n    })\n    return\n  }\n\n  await resetGuildConfiguration(interaction.guildId)\n  await interaction.update({ content: 'Configuração restaurada.', components: [] })\n})\n          "},{"kind":"list","items":["Valide permissões no momento do clique, não apenas ao montar a mensagem.","Nunca confie em IDs ou valores recebidos sem validar existência e escopo.","Para fluxos privados, prefira respostas efêmeras iniciadas pelo próprio usuário.","Evite callbacks duplicados para o mesmo `customId` em módulos diferentes."]}]}],"url":"https://ninenity.vercel.app/doc/callbacks","markdownUrl":"https://ninenity.vercel.app/doc/markdown/callbacks","markdown":"# Callbacks de componentes\n> Trate botões e selects junto da definição visual, com IDs tipados e contexto de interação entregue pelo Core.\n\n- Área: Componentes\n- URL humana: https://ninenity.vercel.app/doc/callbacks\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/callbacks\n- Pacotes e conceitos: setCallback, customId tipado, Auto defer\n\n## Como o callback chega ao Core\n\n1. **O builder registra** — Ao combinar `.setCustomId()` e `.setCallback()`, o ComponentBuilder guarda a função somente em memória.\n2. **O Discord envia o clique** — A interação chega ao `InteractionCreate` com o mesmo `customId`.\n3. **O Core resolve** — O runtime encontra o callback, valida o contexto e entrega `interaction`, `id` e `context`.\n4. **A resposta é concluída** — Se o callback não responder, o Core chama `deferUpdate()` para evitar o estado de falha no componente.\n\n> **Callbacks não são serializados:** Tokens e JSON preservam o layout, mas nunca código executável. Depois de importar um template, conecte os callbacks no código do bot ou use a exportação TypeScript do Builder Studio.\n\n## Callback de botão\n\nDefina o `customId` antes ou depois do callback; o registro acontece quando ambos estiverem presentes.\n\n### approve-button.ts\n\n```text\n\nconst approveButton = new ButtonBuilder()\n  .setCustomId('project:approve')\n  .setLabel('Aprovar projeto')\n  .setStyle(ButtonStyle.Success)\n  .setCallback(async (interaction, id, context) => {\n    await approveProject({\n      action: String(id),\n      userId: interaction.user.id,\n      guildId: interaction.guildId\n    })\n\n    await interaction.update({\n      content: 'Projeto aprovado com sucesso.',\n      components: []\n    })\n\n    console.log('[Component] customId=' + context.customId)\n  })\n```\n\n\nO primeiro argumento é a interação real. O segundo é a parte lógica extraída do `customId`. O terceiro contém o ID completo, valores de select e o tipo do componente.\n\n## IDs segmentados e inferência\n\nO separador `:` transforma segmentos nomeados em um objeto tipado no callback.\n\n### typed-id.ts\n\n```text\n\nnew ButtonBuilder()\n  .setCustomId('project:42:archive')\n  .setLabel('Arquivar')\n  .setStyle(ButtonStyle.Secondary)\n  .setCallback(async (interaction, id) => {\n    // Para IDs segmentados, o editor infere as partes disponíveis.\n    console.log('[Project] callback=' + JSON.stringify(id))\n    await interaction.deferUpdate()\n  })\n```\n\n\n> **Escolha IDs estáveis:** Use IDs curtos que expressem domínio e ação, como `ticket:close` ou `profile:edit`. Dados sensíveis e payloads grandes pertencem ao banco, não ao `customId`.\n\n## Callback de select\n\nOs valores selecionados estão em `interaction.values` e também no `context` normalizado.\n\n### environment-select.ts\n\n```text\n\nconst environmentSelect = new StringSelectMenuBuilder()\n  .setCustomId('project:environment')\n  .setPlaceholder('Escolha o ambiente')\n  .addOptions(\n    option => option.setLabel('Produção').setValue('production'),\n    option => option.setLabel('Desenvolvimento').setValue('development')\n  )\n  .setCallback(async (interaction, _id, context) => {\n    const environment = context.value\n    if (!environment) return\n\n    await projectRepository.setEnvironment(\n      interaction.guildId,\n      environment\n    )\n\n    await interaction.update({\n      content: 'Ambiente alterado para ' + environment,\n      components: []\n    })\n  })\n```\n\n\n### seleção múltipla\n\n```text\n\nnew UserSelectMenuBuilder()\n  .setCustomId('team:members')\n  .setMinValues(1)\n  .setMaxValues(3)\n  .setCallback(async (interaction, _id, context) => {\n    const userIds = context.values\n    await teamRepository.replaceMembers(interaction.guildId, userIds)\n    await interaction.reply({\n      content: userIds.length + ' membros selecionados.',\n      flags: 64\n    })\n  })\n```\n\n\n## Reply, update ou deferUpdate?\n\n| Método | Resultado |\n| --- | --- |\n| `interaction.update()` | Substitui a mensagem que contém o componente |\n| `interaction.reply()` | Cria uma resposta separada ao clique |\n| `interaction.deferUpdate()` | Confirma o clique sem mudar a mensagem |\n| Nenhum | O Core faz `deferUpdate()` automaticamente |\n\n### ação demorada\n\n```text\n\n.setCallback(async interaction => {\n  await interaction.deferReply({ flags: 64 })\n\n  const result = await runLongOperation()\n\n  await interaction.editReply(\n    result.ok ? 'Operação concluída.' : 'Não foi possível concluir.'\n  )\n})\n```\n\n\n## Autorização continua sendo sua regra\n\nO runtime resolve o callback, mas a permissão de negócio precisa ser verificada no handler.\n\n### callback protegido\n\n```text\n\n.setCallback(async interaction => {\n  const member = interaction.member\n  const canManage = member?.permissions?.has('ManageGuild')\n\n  if (!canManage) {\n    await interaction.reply({\n      content: 'Você não pode executar esta ação.',\n      flags: 64\n    })\n    return\n  }\n\n  await resetGuildConfiguration(interaction.guildId)\n  await interaction.update({ content: 'Configuração restaurada.', components: [] })\n})\n```\n\n\n- Valide permissões no momento do clique, não apenas ao montar a mensagem.\n- Nunca confie em IDs ou valores recebidos sem validar existência e escopo.\n- Para fluxos privados, prefira respostas efêmeras iniciadas pelo próprio usuário.\n- Evite callbacks duplicados para o mesmo `customId` em módulos diferentes.\n"},{"slug":"prompts","title":"Prompts","group":"Componentes","eyebrow":"COMPONENTES / PROMPTS","description":"Crie confirmações reutilizáveis, páginas de resultado e instâncias isoladas que funcionam tanto em slash quanto em prefixo.","badges":["Prompt","registerPrompt","promptRequest"],"sections":[{"id":"arquitetura","title":"Definição e solicitação são separadas","blocks":[{"kind":"cards","items":[{"icon":"1","title":"Defina uma vez","text":"O módulo em `Discord/Interactions` monta o prompt e registra um ID estável."},{"icon":"2","title":"Solicite quando precisar","text":"Um comando chama `interaction.promptRequest(id)` para criar a instância."},{"icon":"3","title":"O Core isola","text":"Autor, guild e runtime são vinculados para impedir que outra pessoa controle o fluxo."},{"icon":"4","title":"Resolva a escolha","text":"O botão atualiza para sua página de resultado e executa o callback associado."}]},{"kind":"callout","tone":"info","title":"Por que registrar antes?","text":"O auto-loader importa `Discord/Interactions` no boot. Quando o comando é executado, a definição já existe e pode ser instanciada sem remontar toda a configuração.","icon":"i"}]},{"id":"prompt-simples","title":"Prompt simples com opções automáticas","description":"`Prompt.create()` adiciona uma linha de botões com base nas opções informadas.","blocks":[{"kind":"code","title":"Discord/Interactions/DeletePrompt.ts","code":"\nimport {\n  ButtonStyle,\n  ContainerBuilder,\n  MessageBuilder,\n  Prompt,\n  TextDisplayBuilder\n} from '@ninenity/componentbuilder'\nimport Client from '../../Shared/Settings/Client'\n\nconst prompt = Prompt.create({\n  id: 'delete-prompt',\n  prompt: new MessageBuilder()\n    .addComponents(\n      new ContainerBuilder()\n        .setAccentColor(0xff7582)\n        .addTextDisplayComponents(\n          new TextDisplayBuilder().setContent(\n            '## Excluir projeto?\nEsta ação não poderá ser desfeita.'\n          )\n        )\n    )\n    .template(),\n  options: [\n    { id: 'confirm', label: 'Excluir', style: ButtonStyle.Danger },\n    { id: 'cancel', label: 'Cancelar', style: ButtonStyle.Secondary }\n  ]\n})\n\nClient.registerPrompt('delete-prompt', prompt)\n          "},{"kind":"text","text":"Sem páginas ou callbacks, uma opção apenas conclui o fluxo. Para executar uma regra e mostrar um resultado diferente, use páginas exportadas ou botões com callback como no próximo exemplo."}]},{"id":"paginas-resultado","title":"Prompt com páginas de resultado","description":"A primeira página contém os botões; páginas chamadas `yes` e `no` são escolhidas pelo sufixo do `customId`.","blocks":[{"kind":"code","title":"Discord/Interactions/PublishPrompt.ts","code":"\nconst promptPage = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder()\n      .setAccentColor(0xffc14e)\n      .addTextDisplayComponents(\n        new TextDisplayBuilder().setContent('Deseja publicar esta versão?')\n      )\n      .addActionRowComponents(\n        new ActionRowBuilder<MessageActionRowComponentBuilder>().addComponents(\n          new ButtonBuilder()\n            .setStyle(ButtonStyle.Success)\n            .setLabel('Publicar')\n            .setCustomId('publish-prompt:yes')\n            .setCallback(async interaction => {\n              await publishVersion(interaction.guildId)\n              console.log('[Prompt] versão publicada')\n            }),\n          new ButtonBuilder()\n            .setStyle(ButtonStyle.Secondary)\n            .setLabel('Agora não')\n            .setCustomId('publish-prompt:no')\n        )\n      )\n  )\n  .template()\n\nconst successPage = new MessageBuilder()\n  .addComponents(new ContainerBuilder().addTextDisplayComponents(\n    new TextDisplayBuilder().setContent('✓ Versão publicada com sucesso.')\n  ))\n  .template()\n\nconst canceledPage = new MessageBuilder()\n  .addComponents(new ContainerBuilder().addTextDisplayComponents(\n    new TextDisplayBuilder().setContent('Publicação cancelada.')\n  ))\n  .template()\n\nClient.registerPrompt('publish-prompt', [\n  { id: 'prompt', name: 'prompt', template: promptPage },\n  { id: 'result-yes', name: 'yes', template: successPage },\n  { id: 'result-no', name: 'no', template: canceledPage }\n])\n          "},{"kind":"callout","tone":"success","title":"Integração com Builder Studio","text":"A exportação de múltiplas páginas já produz essa lista. Nomeie as páginas de destino com o mesmo sufixo dos botões, por exemplo `yes`, `no`, `approve` ou `cancel`.","icon":"✓"}]},{"id":"solicitar","title":"Solicitar no comando","description":"O mesmo ID registrado funciona em qualquer interação normalizada pelo Core.","blocks":[{"kind":"code","title":"Discord/Commands/Publish.ts","code":"\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('publish')\n    .setDescription('Confirma a publicação.'),\n  execute: async interaction => {\n    await interaction.promptRequest('publish-prompt', {\n      ephemeral: true\n    })\n  }\n})\n\nClient.prefix({\n  data: new PrefixCommandBuilder()\n    .setName('publish')\n    .setAliases(['publicar']),\n  execute: async interaction => {\n    await interaction.promptRequest('publish-prompt', {\n      ephemeral: true\n    })\n  }\n})\n          "},{"kind":"text","text":"Cada chamada cria uma instância com o autor e a guild da interação. O erro `Prompt \"id\" is not registered` indica que o módulo não foi carregado, o ID diverge ou o arquivo está fora das pastas percorridas."}]},{"id":"definicao-dinamica","title":"Definição dinâmica por usuário","description":"Registre uma factory quando o texto ou as páginas dependem da interação que abriu o prompt.","blocks":[{"kind":"code","title":"prompt factory","code":"\nClient.registerPrompt('remove-member', async context => {\n  const targetId = context.interaction.options?.getUser('usuario')?.id\n  const target = targetId ? await loadMember(targetId) : null\n\n  const page = new MessageBuilder()\n    .addComponents(\n      new ContainerBuilder().addTextDisplayComponents(\n        new TextDisplayBuilder().setContent(\n          'Remover **' + (target?.name ?? 'membro desconhecido') + '**?'\n        )\n      )\n    )\n    .template()\n\n  return Prompt.create({\n    id: 'remove-member',\n    prompt: page,\n    ownerId: context.ownerId,\n    guildId: context.guildId\n  })\n})\n          "},{"kind":"callout","tone":"warning","title":"Não compartilhe estado mutável","text":"Crie os builders dentro da factory quando dados mudam por usuário. Uma definição global com variáveis externas mutáveis pode vazar conteúdo entre duas solicitações simultâneas.","icon":"!"}]}],"url":"https://ninenity.vercel.app/doc/prompts","markdownUrl":"https://ninenity.vercel.app/doc/markdown/prompts","markdown":"# Prompts\n> Crie confirmações reutilizáveis, páginas de resultado e instâncias isoladas que funcionam tanto em slash quanto em prefixo.\n\n- Área: Componentes\n- URL humana: https://ninenity.vercel.app/doc/prompts\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/prompts\n- Pacotes e conceitos: Prompt, registerPrompt, promptRequest\n\n## Definição e solicitação são separadas\n\n- **Defina uma vez**: O módulo em `Discord/Interactions` monta o prompt e registra um ID estável.\n- **Solicite quando precisar**: Um comando chama `interaction.promptRequest(id)` para criar a instância.\n- **O Core isola**: Autor, guild e runtime são vinculados para impedir que outra pessoa controle o fluxo.\n- **Resolva a escolha**: O botão atualiza para sua página de resultado e executa o callback associado.\n\n> **Por que registrar antes?:** O auto-loader importa `Discord/Interactions` no boot. Quando o comando é executado, a definição já existe e pode ser instanciada sem remontar toda a configuração.\n\n## Prompt simples com opções automáticas\n\n`Prompt.create()` adiciona uma linha de botões com base nas opções informadas.\n\n### Discord/Interactions/DeletePrompt.ts\n\n```text\n\nimport {\n  ButtonStyle,\n  ContainerBuilder,\n  MessageBuilder,\n  Prompt,\n  TextDisplayBuilder\n} from '@ninenity/componentbuilder'\nimport Client from '../../Shared/Settings/Client'\n\nconst prompt = Prompt.create({\n  id: 'delete-prompt',\n  prompt: new MessageBuilder()\n    .addComponents(\n      new ContainerBuilder()\n        .setAccentColor(0xff7582)\n        .addTextDisplayComponents(\n          new TextDisplayBuilder().setContent(\n            '## Excluir projeto?\nEsta ação não poderá ser desfeita.'\n          )\n        )\n    )\n    .template(),\n  options: [\n    { id: 'confirm', label: 'Excluir', style: ButtonStyle.Danger },\n    { id: 'cancel', label: 'Cancelar', style: ButtonStyle.Secondary }\n  ]\n})\n\nClient.registerPrompt('delete-prompt', prompt)\n```\n\n\nSem páginas ou callbacks, uma opção apenas conclui o fluxo. Para executar uma regra e mostrar um resultado diferente, use páginas exportadas ou botões com callback como no próximo exemplo.\n\n## Prompt com páginas de resultado\n\nA primeira página contém os botões; páginas chamadas `yes` e `no` são escolhidas pelo sufixo do `customId`.\n\n### Discord/Interactions/PublishPrompt.ts\n\n```text\n\nconst promptPage = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder()\n      .setAccentColor(0xffc14e)\n      .addTextDisplayComponents(\n        new TextDisplayBuilder().setContent('Deseja publicar esta versão?')\n      )\n      .addActionRowComponents(\n        new ActionRowBuilder<MessageActionRowComponentBuilder>().addComponents(\n          new ButtonBuilder()\n            .setStyle(ButtonStyle.Success)\n            .setLabel('Publicar')\n            .setCustomId('publish-prompt:yes')\n            .setCallback(async interaction => {\n              await publishVersion(interaction.guildId)\n              console.log('[Prompt] versão publicada')\n            }),\n          new ButtonBuilder()\n            .setStyle(ButtonStyle.Secondary)\n            .setLabel('Agora não')\n            .setCustomId('publish-prompt:no')\n        )\n      )\n  )\n  .template()\n\nconst successPage = new MessageBuilder()\n  .addComponents(new ContainerBuilder().addTextDisplayComponents(\n    new TextDisplayBuilder().setContent('✓ Versão publicada com sucesso.')\n  ))\n  .template()\n\nconst canceledPage = new MessageBuilder()\n  .addComponents(new ContainerBuilder().addTextDisplayComponents(\n    new TextDisplayBuilder().setContent('Publicação cancelada.')\n  ))\n  .template()\n\nClient.registerPrompt('publish-prompt', [\n  { id: 'prompt', name: 'prompt', template: promptPage },\n  { id: 'result-yes', name: 'yes', template: successPage },\n  { id: 'result-no', name: 'no', template: canceledPage }\n])\n```\n\n\n> **Integração com Builder Studio:** A exportação de múltiplas páginas já produz essa lista. Nomeie as páginas de destino com o mesmo sufixo dos botões, por exemplo `yes`, `no`, `approve` ou `cancel`.\n\n## Solicitar no comando\n\nO mesmo ID registrado funciona em qualquer interação normalizada pelo Core.\n\n### Discord/Commands/Publish.ts\n\n```text\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('publish')\n    .setDescription('Confirma a publicação.'),\n  execute: async interaction => {\n    await interaction.promptRequest('publish-prompt', {\n      ephemeral: true\n    })\n  }\n})\n\nClient.prefix({\n  data: new PrefixCommandBuilder()\n    .setName('publish')\n    .setAliases(['publicar']),\n  execute: async interaction => {\n    await interaction.promptRequest('publish-prompt', {\n      ephemeral: true\n    })\n  }\n})\n```\n\n\nCada chamada cria uma instância com o autor e a guild da interação. O erro `Prompt \"id\" is not registered` indica que o módulo não foi carregado, o ID diverge ou o arquivo está fora das pastas percorridas.\n\n## Definição dinâmica por usuário\n\nRegistre uma factory quando o texto ou as páginas dependem da interação que abriu o prompt.\n\n### prompt factory\n\n```text\n\nClient.registerPrompt('remove-member', async context => {\n  const targetId = context.interaction.options?.getUser('usuario')?.id\n  const target = targetId ? await loadMember(targetId) : null\n\n  const page = new MessageBuilder()\n    .addComponents(\n      new ContainerBuilder().addTextDisplayComponents(\n        new TextDisplayBuilder().setContent(\n          'Remover **' + (target?.name ?? 'membro desconhecido') + '**?'\n        )\n      )\n    )\n    .template()\n\n  return Prompt.create({\n    id: 'remove-member',\n    prompt: page,\n    ownerId: context.ownerId,\n    guildId: context.guildId\n  })\n})\n```\n\n\n> **Não compartilhe estado mutável:** Crie os builders dentro da factory quando dados mudam por usuário. Uma definição global com variáveis externas mutáveis pode vazar conteúdo entre duas solicitações simultâneas.\n"},{"slug":"paginacao","title":"Paginação","group":"Componentes","eyebrow":"COMPONENTES / PAGINAÇÃO","description":"Crie paginação estática, dinâmica ou híbrida e personalize completamente os botões de navegação.","badges":["Static","Dynamic","Hybrid","Custom controls"],"sections":[{"id":"escolher-tipo","title":"Escolha o tipo certo","blocks":[{"kind":"table","columns":["Tipo","Fonte","Melhor uso"],"rows":[["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"]]},{"kind":"callout","tone":"info","title":"Estado é zero-based internamente","text":"`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.","icon":"i"}]},{"id":"estatica","title":"Paginação estática","description":"Registre uma lista quando cada página já é conhecida durante o carregamento.","blocks":[{"kind":"code","title":"Discord/Interactions/GuidePages.ts","code":"\nconst page = (title: string, content: string) =>\n  new MessageBuilder()\n    .addComponents(\n      new ContainerBuilder()\n        .setAccentColor(0xa571f4)\n        .addTextDisplayComponents(\n          new TextDisplayBuilder().setContent(\n            '## ' + title + '\n' + content + '\n\nPágina ${pages.current}/${pages.total}'\n          )\n        )\n    )\n    .template()\n\nClient.registerPaginator('guide-pages', [\n  { id: 'intro', name: 'Introdução', template: page('Introdução', 'Como usar o painel.') },\n  { id: 'config', name: 'Configuração', template: page('Configuração', 'Escolha seus canais.') },\n  { id: 'finish', name: 'Finalização', template: page('Tudo pronto', 'Revise e confirme.') }\n])\n          "},{"kind":"code","title":"Discord/Commands/Guide.ts","code":"\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('guide')\n    .setDescription('Abre o guia paginado.'),\n  execute: async interaction => {\n    await interaction.paginatorRequest('guide-pages', {\n      ephemeral: true,\n      pageIndex: 0\n    })\n  }\n})\n          "}]},{"id":"dinamica","title":"Paginação dinâmica","description":"O paginator divide `data`, chama `render` para a fatia atual e injeta o contexto da página.","blocks":[{"kind":"code","title":"Discord/Interactions/UserPages.ts","code":"\ntype UserRecord = {\n  id: string\n  name: string\n  level: number\n}\n\nconst users: UserRecord[] = await userRepository.list()\n\nconst paginator = Paginator.dynamic({\n  id: 'users-pages',\n  data: users,\n  pageSize: 5,\n  render: async (items, context) => {\n    const rows = items.map((user, index) =>\n      (context.pageIndex * 5 + index + 1) +\n      '. **' + user.name + '** — nível ' + user.level\n    )\n\n    return new MessageBuilder()\n      .addComponents(\n        new ContainerBuilder().addTextDisplayComponents(\n          new TextDisplayBuilder().setContent(\n            '## Usuários\n' + rows.join('\n') +\n            '\n\nPágina ' + context.page + '/' + context.pageCount\n          )\n        )\n      )\n      .template()\n  }\n})\n\nClient.registerPaginator('users-pages', paginator)\n          "},{"kind":"table","columns":["Contexto","Valor"],"rows":[["`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"]]}]},{"id":"factory-dinamica","title":"Dados novos a cada solicitação","description":"Use uma definição factory para consultar banco ou API quando o usuário abre a paginação, não durante o boot.","blocks":[{"kind":"code","title":"paginator factory","code":"\nClient.registerPaginator('audit-pages', async context => {\n  const guildId = context.guildId\n  const records = guildId ? await auditRepository.list(guildId) : []\n\n  return Paginator.dynamic({\n    id: 'audit-pages',\n    data: records,\n    pageSize: 10,\n    ownerId: context.ownerId,\n    guildId,\n    render: (items, page) => new MessageBuilder()\n      .addComponents(\n        new ContainerBuilder().addTextDisplayComponents(\n          new TextDisplayBuilder().setContent(\n            '## Auditoria\n' +\n            items.map(item => '- ' + item.summary).join('\n') +\n            '\n\n' + page.page + '/' + page.pageCount\n          )\n        )\n      )\n      .template()\n  })\n})\n          "},{"kind":"callout","tone":"success","title":"Snapshot consistente","text":"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.","icon":"✓"}]},{"id":"hibrida","title":"Paginação híbrida","description":"A primeira página é fixa; as seguintes vêm de uma coleção dinâmica.","blocks":[{"kind":"code","title":"catalog-paginator.ts","code":"\nconst intro = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder()\n      .setAccentColor(0x6485ff)\n      .addTextDisplayComponents(\n        new TextDisplayBuilder().setContent(\n          '## Catálogo Ninenity\nUse **Próxima** para explorar os projetos.'\n        )\n      )\n  )\n  .template()\n\nconst paginator = Paginator.hybrid({\n  id: 'catalog-pages',\n  intro,\n  data: projects,\n  pageSize: 3,\n  render: (items, context) => new MessageBuilder()\n    .addComponents(\n      new ContainerBuilder().addTextDisplayComponents(\n        new TextDisplayBuilder().setContent(\n          items.map(project => '### ' + project.name + '\n' + project.summary).join('\n\n') +\n          '\n\nPágina ' + context.page + '/' + context.pageCount\n        )\n      )\n    )\n    .template()\n})\n\nClient.registerPaginator('catalog-pages', paginator)\n          "}]},{"id":"controles-customizados","title":"Botões de paginação personalizados","description":"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.","blocks":[{"kind":"code","title":"custom-controls.ts","code":"\nconst controls = new ActionRowBuilder<MessageActionRowComponentBuilder>()\n  .addComponents(\n    new ButtonBuilder()\n      .setCustomId('guide-pages:first')\n      .setLabel('Primeira')\n      .setEmoji('⏮')\n      .setStyle(ButtonStyle.Secondary)\n      .setPaginationAction('first'),\n    new ButtonBuilder()\n      .setCustomId('guide-pages:back')\n      .setLabel('Voltar')\n      .setEmoji('◀')\n      .setStyle(ButtonStyle.Secondary)\n      .setPaginationAction('back'),\n    new ButtonBuilder()\n      .setCustomId('guide-pages:next')\n      .setLabel('Próxima')\n      .setEmoji('▶')\n      .setStyle(ButtonStyle.Primary)\n      .setPaginationAction('next'),\n    new ButtonBuilder()\n      .setCustomId('guide-pages:last')\n      .setLabel('Última')\n      .setEmoji('⏭')\n      .setStyle(ButtonStyle.Secondary)\n      .setPaginationAction('last')\n  )\n\nconst page = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder()\n      .addTextDisplayComponents(\n        new TextDisplayBuilder().setContent(\n          'Página ${pages.current} de ${pages.total}'\n        )\n      )\n      .addActionRowComponents(controls)\n  )\n  .template()\n          "},{"kind":"list","items":["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."]}]},{"id":"controles-automaticos","title":"Personalizar rótulos automáticos","description":"Se as páginas não incluem controles, `Paginator` cria a linha e você pode trocar os rótulos.","blocks":[{"kind":"code","title":"automatic-controls.ts","code":"\nconst paginator = Paginator.static({\n  id: 'help-pages',\n  pages: [introPage, commandsPage, settingsPage],\n  controls: {\n    first: 'Início',\n    previous: 'Voltar',\n    next: 'Avançar',\n    last: 'Fim'\n  }\n})\n\nClient.registerPaginator('help-pages', paginator)\n          "},{"kind":"callout","tone":"warning","title":"Não duplique controles","text":"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á.","icon":"!"}]}],"url":"https://ninenity.vercel.app/doc/paginacao","markdownUrl":"https://ninenity.vercel.app/doc/markdown/paginacao","markdown":"# Paginação\n> Crie paginação estática, dinâmica ou híbrida e personalize completamente os botões de navegação.\n\n- Área: Componentes\n- URL humana: https://ninenity.vercel.app/doc/paginacao\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/paginacao\n- Pacotes e conceitos: Static, Dynamic, Hybrid, Custom controls\n\n## Escolha o tipo certo\n\n| Tipo | Fonte | Melhor uso |\n| --- | --- | --- |\n| Estática | Lista pronta de templates | Tutoriais, painéis e páginas com layout diferente |\n| Dinâmica | Array dividido por `pageSize` | Usuários, logs, ranking e resultados de banco |\n| Híbrida | Introdução fixa + array dinâmico | Catálogo com capa ou instruções antes dos itens |\n\n> **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.\n\n## Paginação estática\n\nRegistre uma lista quando cada página já é conhecida durante o carregamento.\n\n### Discord/Interactions/GuidePages.ts\n\n```text\n\nconst page = (title: string, content: string) =>\n  new MessageBuilder()\n    .addComponents(\n      new ContainerBuilder()\n        .setAccentColor(0xa571f4)\n        .addTextDisplayComponents(\n          new TextDisplayBuilder().setContent(\n            '## ' + title + '\n' + content + '\n\nPágina ${pages.current}/${pages.total}'\n          )\n        )\n    )\n    .template()\n\nClient.registerPaginator('guide-pages', [\n  { id: 'intro', name: 'Introdução', template: page('Introdução', 'Como usar o painel.') },\n  { id: 'config', name: 'Configuração', template: page('Configuração', 'Escolha seus canais.') },\n  { id: 'finish', name: 'Finalização', template: page('Tudo pronto', 'Revise e confirme.') }\n])\n```\n\n\n### Discord/Commands/Guide.ts\n\n```text\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('guide')\n    .setDescription('Abre o guia paginado.'),\n  execute: async interaction => {\n    await interaction.paginatorRequest('guide-pages', {\n      ephemeral: true,\n      pageIndex: 0\n    })\n  }\n})\n```\n\n\n## Paginação dinâmica\n\nO paginator divide `data`, chama `render` para a fatia atual e injeta o contexto da página.\n\n### Discord/Interactions/UserPages.ts\n\n```text\n\ntype UserRecord = {\n  id: string\n  name: string\n  level: number\n}\n\nconst users: UserRecord[] = await userRepository.list()\n\nconst paginator = Paginator.dynamic({\n  id: 'users-pages',\n  data: users,\n  pageSize: 5,\n  render: async (items, context) => {\n    const rows = items.map((user, index) =>\n      (context.pageIndex * 5 + index + 1) +\n      '. **' + user.name + '** — nível ' + user.level\n    )\n\n    return new MessageBuilder()\n      .addComponents(\n        new ContainerBuilder().addTextDisplayComponents(\n          new TextDisplayBuilder().setContent(\n            '## Usuários\n' + rows.join('\n') +\n            '\n\nPágina ' + context.page + '/' + context.pageCount\n          )\n        )\n      )\n      .template()\n  }\n})\n\nClient.registerPaginator('users-pages', paginator)\n```\n\n\n| Contexto | Valor |\n| --- | --- |\n| `items` | Itens da página atual |\n| `page` | Número amigável, começando em 1 |\n| `pageIndex` | Índice interno, começando em 0 |\n| `pageCount` | Quantidade total de páginas |\n| `totalItems` | Quantidade total de registros |\n\n## Dados novos a cada solicitação\n\nUse uma definição factory para consultar banco ou API quando o usuário abre a paginação, não durante o boot.\n\n### paginator factory\n\n```text\n\nClient.registerPaginator('audit-pages', async context => {\n  const guildId = context.guildId\n  const records = guildId ? await auditRepository.list(guildId) : []\n\n  return Paginator.dynamic({\n    id: 'audit-pages',\n    data: records,\n    pageSize: 10,\n    ownerId: context.ownerId,\n    guildId,\n    render: (items, page) => new MessageBuilder()\n      .addComponents(\n        new ContainerBuilder().addTextDisplayComponents(\n          new TextDisplayBuilder().setContent(\n            '## Auditoria\n' +\n            items.map(item => '- ' + item.summary).join('\n') +\n            '\n\n' + page.page + '/' + page.pageCount\n          )\n        )\n      )\n      .template()\n  })\n})\n```\n\n\n> **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.\n\n## Paginação híbrida\n\nA primeira página é fixa; as seguintes vêm de uma coleção dinâmica.\n\n### catalog-paginator.ts\n\n```text\n\nconst intro = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder()\n      .setAccentColor(0x6485ff)\n      .addTextDisplayComponents(\n        new TextDisplayBuilder().setContent(\n          '## Catálogo Ninenity\nUse **Próxima** para explorar os projetos.'\n        )\n      )\n  )\n  .template()\n\nconst paginator = Paginator.hybrid({\n  id: 'catalog-pages',\n  intro,\n  data: projects,\n  pageSize: 3,\n  render: (items, context) => new MessageBuilder()\n    .addComponents(\n      new ContainerBuilder().addTextDisplayComponents(\n        new TextDisplayBuilder().setContent(\n          items.map(project => '### ' + project.name + '\n' + project.summary).join('\n\n') +\n          '\n\nPágina ' + context.page + '/' + context.pageCount\n        )\n      )\n    )\n    .template()\n})\n\nClient.registerPaginator('catalog-pages', paginator)\n```\n\n\n## Botões de paginação personalizados\n\nInclua 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.\n\n### custom-controls.ts\n\n```text\n\nconst controls = new ActionRowBuilder<MessageActionRowComponentBuilder>()\n  .addComponents(\n    new ButtonBuilder()\n      .setCustomId('guide-pages:first')\n      .setLabel('Primeira')\n      .setEmoji('⏮')\n      .setStyle(ButtonStyle.Secondary)\n      .setPaginationAction('first'),\n    new ButtonBuilder()\n      .setCustomId('guide-pages:back')\n      .setLabel('Voltar')\n      .setEmoji('◀')\n      .setStyle(ButtonStyle.Secondary)\n      .setPaginationAction('back'),\n    new ButtonBuilder()\n      .setCustomId('guide-pages:next')\n      .setLabel('Próxima')\n      .setEmoji('▶')\n      .setStyle(ButtonStyle.Primary)\n      .setPaginationAction('next'),\n    new ButtonBuilder()\n      .setCustomId('guide-pages:last')\n      .setLabel('Última')\n      .setEmoji('⏭')\n      .setStyle(ButtonStyle.Secondary)\n      .setPaginationAction('last')\n  )\n\nconst page = new MessageBuilder()\n  .addComponents(\n    new ContainerBuilder()\n      .addTextDisplayComponents(\n        new TextDisplayBuilder().setContent(\n          'Página ${pages.current} de ${pages.total}'\n        )\n      )\n      .addActionRowComponents(controls)\n  )\n  .template()\n```\n\n\n- O prefixo do `customId` deve ser exatamente o ID registrado no paginator.\n- `back` e `previous` são entendidos como a página anterior; no builder use `setPaginationAction('back')`.\n- Use `first` e `last` apenas quando a quantidade de páginas justifica esses atalhos.\n- O runtime hidrata os tokens `pages.current` e `pages.total` antes de enviar.\n\n## Personalizar rótulos automáticos\n\nSe as páginas não incluem controles, `Paginator` cria a linha e você pode trocar os rótulos.\n\n### automatic-controls.ts\n\n```text\n\nconst paginator = Paginator.static({\n  id: 'help-pages',\n  pages: [introPage, commandsPage, settingsPage],\n  controls: {\n    first: 'Início',\n    previous: 'Voltar',\n    next: 'Avançar',\n    last: 'Fim'\n  }\n})\n\nClient.registerPaginator('help-pages', paginator)\n```\n\n\n> **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á.\n"},{"slug":"configuracao","title":"Configuração","group":"Operação","eyebrow":"OPERAÇÃO / CONFIGURAÇÃO","description":"Leia variáveis, canais e cargos globais ou específicos de cada guild sem espalhar IDs pelo código.","badges":["appConfig","Channels","Roles","HUB"],"sections":[{"id":"origem","title":"De onde vem a configuração","description":"O Client começa com defaults locais e pode ser hidratado pela API quando as credenciais estão presentes.","blocks":[{"kind":"steps","items":[{"title":"Defaults locais","text":"O objeto `config` passado ao `LibsClient` garante valores básicos mesmo sem API."},{"title":"Hidratação","text":"Com `API_KEY` e `API_SECRET`, o Core solicita a configuração persistida do bot."},{"title":"Resolução por guild","text":"`appConfig(guildId?)` mescla a base global com a entrada em `guilds[guildId]`."},{"title":"Contexto automático","text":"Em comandos, eventos, callbacks e tasks por guild, o ID atual é resolvido pelo runtime."}]},{"kind":"callout","tone":"info","title":"Sem guild, retorno seguro e vazio","text":"Fora de um contexto de servidor, chame `Client.appConfig(guildId)`. Sem contexto e sem ID explícito, o Core não escolhe uma guild arbitrária.","icon":"◎"}]},{"id":"estrutura-config","title":"Estrutura persistida","description":"A configuração base e as sobrescritas de cada guild seguem o mesmo formato.","blocks":[{"kind":"code","title":"exemplo conceitual de AppConfig","language":"json","code":"\n{\n  \"channels\": {\n    \"support\": \"123456789012345678\"\n  },\n  \"roles\": {\n    \"staff\": \"223456789012345678\"\n  },\n  \"variables\": {\n    \"general\": {\n      \"prefix\": { \"type\": \"string\", \"value\": \"!\" },\n      \"maxTickets\": { \"type\": \"number\", \"value\": 3 },\n      \"features\": { \"type\": \"list\", \"value\": [\"tickets\", \"logs\"] }\n    }\n  },\n  \"guilds\": {\n    \"323456789012345678\": {\n      \"channels\": { \"support\": \"423456789012345678\" },\n      \"variables\": {\n        \"general\": {\n          \"prefix\": { \"type\": \"string\", \"value\": \"?\" }\n        }\n      }\n    }\n  }\n}\n          "},{"kind":"table","columns":["Tipo","Valor resolvido"],"rows":[["`string`","Texto"],["`number`","Número, sem conversão para string"],["`object`","Objeto JSON de chave e valor"],["`list`","Array JSON ordenado"]]}]},{"id":"ler-variaveis","title":"Ler variáveis no contexto atual","description":"O runtime remove os wrappers `type/value`; sua lógica recebe os valores puros.","blocks":[{"kind":"code","title":"configuração dentro de comando","code":"\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('settings')\n    .setDescription('Mostra as configurações atuais.'),\n  execute: async interaction => {\n    const config = Client.appConfig()\n    const general = config.variables?.general ?? {}\n\n    await interaction.reply({\n      content: [\n        'Prefixo: ' + (general.prefix ?? '!'),\n        'Máximo de tickets: ' + (general.maxTickets ?? 1),\n        'Recursos: ' + (general.features ?? []).join(', ')\n      ].join('\n'),\n      flags: 64\n    })\n  }\n})\n          "},{"kind":"code","title":"fora de uma interação","code":"\nasync function refreshGuild(guildId: string) {\n  const config = Client.appConfig(guildId)\n  const enabled = config.variables?.automation?.enabled ?? false\n\n  if (!enabled) return\n  await synchronizeGuild(guildId, config)\n}\n          "}]},{"id":"canais-cargos","title":"Aliases de canais e cargos","description":"Aliases tornam o código legível e permitem trocar IDs no HUB sem editar o bot.","blocks":[{"kind":"code","title":"uso de aliases","code":"\nconst channels = Client.getAppChannels()\nconst roles = Client.getAppRoles()\n\nconst supportChannel = channels.support\nconst staffRole = roles.staff\n\nif (supportChannel) {\n  console.log(\n    '[Config] support=' + supportChannel.Name +\n    ' id=' + supportChannel.Id\n  )\n}\n\nawait interaction.reply({\n  content: [\n    'Suporte: ' + (supportChannel?.Mention ?? 'não configurado'),\n    'Equipe: ' + (staffRole?.Mention ?? 'não configurado')\n  ].join('\n'),\n  flags: 64\n})\n          "},{"kind":"table","columns":["Campo","Canal","Cargo"],"rows":[["ID","`Id`","`Id`"],["Nome","`Name`","`Name`"],["Menção","`Mention` / `toString()`","`Mention` / `toString()`"],["Metadado","`Type`","`Color` e `Position`"]]}]},{"id":"defaults","title":"Defaults locais","description":"Defaults permitem desenvolver sem depender da API e documentam o formato esperado.","blocks":[{"kind":"code","title":"Shared/Settings/Client.ts","code":"\nconst Client = new LibsClient<AppConfig>({\n  intents: [GatewayIntentBits.Guilds],\n  token: process.env.BOT_TOKEN,\n  config: {\n    channels: {},\n    roles: {},\n    variables: {\n      general: {\n        prefix: { type: 'string', value: '!' },\n        maxTickets: { type: 'number', value: 1 },\n        features: { type: 'list', value: [] }\n      }\n    }\n  }\n})\n          "},{"kind":"callout","tone":"warning","title":"Defaults não são secrets","text":"Tokens, senhas e chaves de API pertencem ao `.env` ou a um cofre de secrets. A configuração do bot é adequada para comportamento, aliases e valores operacionais.","icon":"!"}]}],"url":"https://ninenity.vercel.app/doc/configuracao","markdownUrl":"https://ninenity.vercel.app/doc/markdown/configuracao","markdown":"# Configuração\n> Leia variáveis, canais e cargos globais ou específicos de cada guild sem espalhar IDs pelo código.\n\n- Área: Operação\n- URL humana: https://ninenity.vercel.app/doc/configuracao\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/configuracao\n- Pacotes e conceitos: appConfig, Channels, Roles, HUB\n\n## De onde vem a configuração\n\nO Client começa com defaults locais e pode ser hidratado pela API quando as credenciais estão presentes.\n\n1. **Defaults locais** — O objeto `config` passado ao `LibsClient` garante valores básicos mesmo sem API.\n2. **Hidratação** — Com `API_KEY` e `API_SECRET`, o Core solicita a configuração persistida do bot.\n3. **Resolução por guild** — `appConfig(guildId?)` mescla a base global com a entrada em `guilds[guildId]`.\n4. **Contexto automático** — Em comandos, eventos, callbacks e tasks por guild, o ID atual é resolvido pelo runtime.\n\n> **Sem guild, retorno seguro e vazio:** Fora de um contexto de servidor, chame `Client.appConfig(guildId)`. Sem contexto e sem ID explícito, o Core não escolhe uma guild arbitrária.\n\n## Estrutura persistida\n\nA configuração base e as sobrescritas de cada guild seguem o mesmo formato.\n\n### exemplo conceitual de AppConfig\n\n```json\n\n{\n  \"channels\": {\n    \"support\": \"123456789012345678\"\n  },\n  \"roles\": {\n    \"staff\": \"223456789012345678\"\n  },\n  \"variables\": {\n    \"general\": {\n      \"prefix\": { \"type\": \"string\", \"value\": \"!\" },\n      \"maxTickets\": { \"type\": \"number\", \"value\": 3 },\n      \"features\": { \"type\": \"list\", \"value\": [\"tickets\", \"logs\"] }\n    }\n  },\n  \"guilds\": {\n    \"323456789012345678\": {\n      \"channels\": { \"support\": \"423456789012345678\" },\n      \"variables\": {\n        \"general\": {\n          \"prefix\": { \"type\": \"string\", \"value\": \"?\" }\n        }\n      }\n    }\n  }\n}\n```\n\n\n| Tipo | Valor resolvido |\n| --- | --- |\n| `string` | Texto |\n| `number` | Número, sem conversão para string |\n| `object` | Objeto JSON de chave e valor |\n| `list` | Array JSON ordenado |\n\n## Ler variáveis no contexto atual\n\nO runtime remove os wrappers `type/value`; sua lógica recebe os valores puros.\n\n### configuração dentro de comando\n\n```text\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('settings')\n    .setDescription('Mostra as configurações atuais.'),\n  execute: async interaction => {\n    const config = Client.appConfig()\n    const general = config.variables?.general ?? {}\n\n    await interaction.reply({\n      content: [\n        'Prefixo: ' + (general.prefix ?? '!'),\n        'Máximo de tickets: ' + (general.maxTickets ?? 1),\n        'Recursos: ' + (general.features ?? []).join(', ')\n      ].join('\n'),\n      flags: 64\n    })\n  }\n})\n```\n\n\n### fora de uma interação\n\n```text\n\nasync function refreshGuild(guildId: string) {\n  const config = Client.appConfig(guildId)\n  const enabled = config.variables?.automation?.enabled ?? false\n\n  if (!enabled) return\n  await synchronizeGuild(guildId, config)\n}\n```\n\n\n## Aliases de canais e cargos\n\nAliases tornam o código legível e permitem trocar IDs no HUB sem editar o bot.\n\n### uso de aliases\n\n```text\n\nconst channels = Client.getAppChannels()\nconst roles = Client.getAppRoles()\n\nconst supportChannel = channels.support\nconst staffRole = roles.staff\n\nif (supportChannel) {\n  console.log(\n    '[Config] support=' + supportChannel.Name +\n    ' id=' + supportChannel.Id\n  )\n}\n\nawait interaction.reply({\n  content: [\n    'Suporte: ' + (supportChannel?.Mention ?? 'não configurado'),\n    'Equipe: ' + (staffRole?.Mention ?? 'não configurado')\n  ].join('\n'),\n  flags: 64\n})\n```\n\n\n| Campo | Canal | Cargo |\n| --- | --- | --- |\n| ID | `Id` | `Id` |\n| Nome | `Name` | `Name` |\n| Menção | `Mention` / `toString()` | `Mention` / `toString()` |\n| Metadado | `Type` | `Color` e `Position` |\n\n## Defaults locais\n\nDefaults permitem desenvolver sem depender da API e documentam o formato esperado.\n\n### Shared/Settings/Client.ts\n\n```text\n\nconst Client = new LibsClient<AppConfig>({\n  intents: [GatewayIntentBits.Guilds],\n  token: process.env.BOT_TOKEN,\n  config: {\n    channels: {},\n    roles: {},\n    variables: {\n      general: {\n        prefix: { type: 'string', value: '!' },\n        maxTickets: { type: 'number', value: 1 },\n        features: { type: 'list', value: [] }\n      }\n    }\n  }\n})\n```\n\n\n> **Defaults não são secrets:** Tokens, senhas e chaves de API pertencem ao `.env` ou a um cofre de secrets. A configuração do bot é adequada para comportamento, aliases e valores operacionais.\n"},{"slug":"multi-guild","title":"Multi-guild","group":"Operação","eyebrow":"OPERAÇÃO / MULTI-GUILD","description":"Mantenha configuração, recursos, filas e interações independentes quando o mesmo bot atende vários servidores.","badges":["Context isolation","Guild resources","Scoped runtime"],"sections":[{"id":"o-problema","title":"O problema que o escopo resolve","description":"Sem isolamento, duas guilds podem disputar a mesma configuração, fila ou instância de componente.","blocks":[{"kind":"cards","columns":3,"items":[{"icon":"C","title":"Configuração","text":"Cada guild possui canais, cargos e variáveis próprios."},{"icon":"Q","title":"Concorrência","text":"Filas lógicas recebem a guild na chave interna quando há contexto."},{"icon":"I","title":"Interações","text":"Prompts e paginações validam autor, guild e Client responsável."}]},{"kind":"callout","tone":"success","title":"Com contexto, a API continua simples","text":"Dentro de comandos, eventos, callbacks, modais e tasks por guild, continue usando `Client.appConfig()` sem argumentos. O runtime mantém a guild ativa durante a execução.","icon":"✓"}]},{"id":"contexto-automatico","title":"Contexto automático","blocks":[{"kind":"code","title":"mesma função em qualquer guild","code":"\nasync function sendSupportPanel() {\n  const config = Client.appConfig()\n  const channel = Client.getAppChannels().support\n  const staff = Client.getAppRoles().staff\n\n  if (!channel) return\n\n  await sendPanel(channel.Id, {\n    title: config.variables?.tickets?.title ?? 'Suporte',\n    staffRoleId: staff?.Id\n  })\n}\n\nClient.on(Events.GuildMemberAdd, {\n  execute: async member => {\n    // O evento ativa o contexto de member.guild.id.\n    await sendSupportPanel()\n  }\n})\n          "},{"kind":"text","text":"A função `sendSupportPanel` não recebe `guildId`, porque foi chamada durante um evento contextualizado. Se a mesma função também rodar em um script administrativo sem contexto, passe o ID explicitamente em sua assinatura."}]},{"id":"contexto-explicito","title":"Contexto explícito","description":"Jobs externos, scripts e callbacks fora do runtime precisam indicar a guild que desejam ler.","blocks":[{"kind":"code","title":"sincronização externa","code":"\nasync function synchronizeAllGuilds() {\n  for (const guild of Client.guilds.cache.values()) {\n    const config = Client.appConfig(guild.id)\n    const channels = Client.getAppChannels(guild.id)\n    const roles = Client.getAppRoles(guild.id)\n\n    await syncGuild({\n      guildId: guild.id,\n      auditChannelId: channels.audit?.Id,\n      staffRoleId: roles.staff?.Id,\n      options: config.variables?.sync ?? {}\n    })\n  }\n}\n          "},{"kind":"callout","tone":"warning","title":"Nunca use a primeira guild do cache como fallback","text":"`Client.guilds.cache.first()` torna o comportamento dependente da ordem de conexão. Se a operação é por guild, o ID deve vir do contexto, argumento ou dado persistido.","icon":"!"}]},{"id":"fila-cooldown","title":"Filas e cooldowns","description":"O Core acrescenta o escopo quando a chamada ocorre dentro de uma execução contextualizada.","blocks":[{"kind":"code","title":"fila por recurso e guild","code":"\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('sync')\n    .setDescription('Sincroniza o painel.'),\n  cooldown: '15s',\n  execute: async interaction => {\n    await interaction.deferReply({ flags: 64 })\n\n    await Client.queue('panel-sync', async () => {\n      await syncPanel(interaction.guildId)\n    })\n\n    await interaction.editReply('Painel sincronizado.')\n  }\n})\n          "},{"kind":"text","text":"Duas execuções de `panel-sync` na mesma guild entram na mesma fila. A mesma chave em outra guild segue independentemente. O cooldown de comando também considera a guild para não bloquear o usuário em todos os servidores."}]},{"id":"componentes-escopados","title":"Prompts e paginações escopados","description":"Cada solicitação registra uma instância temporária vinculada ao autor e à guild.","blocks":[{"kind":"code","title":"factory escopada","code":"\nClient.registerPaginator('members-pages', async context => {\n  const members = context.guildId\n    ? await memberRepository.list(context.guildId)\n    : []\n\n  return Paginator.dynamic({\n    id: 'members-pages',\n    ownerId: context.ownerId,\n    guildId: context.guildId,\n    data: members,\n    pageSize: 10,\n    render: renderMembersPage\n  })\n})\n          "},{"kind":"list","items":["Outro usuário não consegue navegar pela instância aberta pelo autor.","Um componente enviado em uma guild não é resolvido pela instância de outra guild.","Dois Clients no mesmo processo usam namespaces de runtime separados.","Dados retornados por uma factory devem ser consultados usando `context.guildId`."]}]},{"id":"checklist-multi","title":"Checklist multi-guild","blocks":[{"kind":"list","items":["Toda tabela ou coleção persistente inclui `guildId` quando os dados pertencem ao servidor.","Caches criados pela aplicação usam `Map<guildId, value>` ou uma chave composta.","IDs de canais e cargos vêm de aliases resolvidos, não de constantes globais.","Factories de prompts e paginações usam `context.guildId`.","Tasks de escopo `guild` não gravam o resultado em uma variável global única.","Logs de operação incluem a guild quando isso ajuda a diagnosticar a origem."]}]}],"url":"https://ninenity.vercel.app/doc/multi-guild","markdownUrl":"https://ninenity.vercel.app/doc/markdown/multi-guild","markdown":"# Multi-guild\n> Mantenha configuração, recursos, filas e interações independentes quando o mesmo bot atende vários servidores.\n\n- Área: Operação\n- URL humana: https://ninenity.vercel.app/doc/multi-guild\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/multi-guild\n- Pacotes e conceitos: Context isolation, Guild resources, Scoped runtime\n\n## O problema que o escopo resolve\n\nSem isolamento, duas guilds podem disputar a mesma configuração, fila ou instância de componente.\n\n- **Configuração**: Cada guild possui canais, cargos e variáveis próprios.\n- **Concorrência**: Filas lógicas recebem a guild na chave interna quando há contexto.\n- **Interações**: Prompts e paginações validam autor, guild e Client responsável.\n\n> **Com contexto, a API continua simples:** Dentro de comandos, eventos, callbacks, modais e tasks por guild, continue usando `Client.appConfig()` sem argumentos. O runtime mantém a guild ativa durante a execução.\n\n## Contexto automático\n\n### mesma função em qualquer guild\n\n```text\n\nasync function sendSupportPanel() {\n  const config = Client.appConfig()\n  const channel = Client.getAppChannels().support\n  const staff = Client.getAppRoles().staff\n\n  if (!channel) return\n\n  await sendPanel(channel.Id, {\n    title: config.variables?.tickets?.title ?? 'Suporte',\n    staffRoleId: staff?.Id\n  })\n}\n\nClient.on(Events.GuildMemberAdd, {\n  execute: async member => {\n    // O evento ativa o contexto de member.guild.id.\n    await sendSupportPanel()\n  }\n})\n```\n\n\nA função `sendSupportPanel` não recebe `guildId`, porque foi chamada durante um evento contextualizado. Se a mesma função também rodar em um script administrativo sem contexto, passe o ID explicitamente em sua assinatura.\n\n## Contexto explícito\n\nJobs externos, scripts e callbacks fora do runtime precisam indicar a guild que desejam ler.\n\n### sincronização externa\n\n```text\n\nasync function synchronizeAllGuilds() {\n  for (const guild of Client.guilds.cache.values()) {\n    const config = Client.appConfig(guild.id)\n    const channels = Client.getAppChannels(guild.id)\n    const roles = Client.getAppRoles(guild.id)\n\n    await syncGuild({\n      guildId: guild.id,\n      auditChannelId: channels.audit?.Id,\n      staffRoleId: roles.staff?.Id,\n      options: config.variables?.sync ?? {}\n    })\n  }\n}\n```\n\n\n> **Nunca use a primeira guild do cache como fallback:** `Client.guilds.cache.first()` torna o comportamento dependente da ordem de conexão. Se a operação é por guild, o ID deve vir do contexto, argumento ou dado persistido.\n\n## Filas e cooldowns\n\nO Core acrescenta o escopo quando a chamada ocorre dentro de uma execução contextualizada.\n\n### fila por recurso e guild\n\n```text\n\nClient.slash({\n  data: new SlashCommandBuilder()\n    .setName('sync')\n    .setDescription('Sincroniza o painel.'),\n  cooldown: '15s',\n  execute: async interaction => {\n    await interaction.deferReply({ flags: 64 })\n\n    await Client.queue('panel-sync', async () => {\n      await syncPanel(interaction.guildId)\n    })\n\n    await interaction.editReply('Painel sincronizado.')\n  }\n})\n```\n\n\nDuas execuções de `panel-sync` na mesma guild entram na mesma fila. A mesma chave em outra guild segue independentemente. O cooldown de comando também considera a guild para não bloquear o usuário em todos os servidores.\n\n## Prompts e paginações escopados\n\nCada solicitação registra uma instância temporária vinculada ao autor e à guild.\n\n### factory escopada\n\n```text\n\nClient.registerPaginator('members-pages', async context => {\n  const members = context.guildId\n    ? await memberRepository.list(context.guildId)\n    : []\n\n  return Paginator.dynamic({\n    id: 'members-pages',\n    ownerId: context.ownerId,\n    guildId: context.guildId,\n    data: members,\n    pageSize: 10,\n    render: renderMembersPage\n  })\n})\n```\n\n\n- Outro usuário não consegue navegar pela instância aberta pelo autor.\n- Um componente enviado em uma guild não é resolvido pela instância de outra guild.\n- Dois Clients no mesmo processo usam namespaces de runtime separados.\n- Dados retornados por uma factory devem ser consultados usando `context.guildId`.\n\n## Checklist multi-guild\n\n- Toda tabela ou coleção persistente inclui `guildId` quando os dados pertencem ao servidor.\n- Caches criados pela aplicação usam `Map<guildId, value>` ou uma chave composta.\n- IDs de canais e cargos vêm de aliases resolvidos, não de constantes globais.\n- Factories de prompts e paginações usam `context.guildId`.\n- Tasks de escopo `guild` não gravam o resultado em uma variável global única.\n- Logs de operação incluem a guild quando isso ajuda a diagnosticar a origem.\n"},{"slug":"logs","title":"Logs e diagnóstico","group":"Operação","eyebrow":"OPERAÇÃO / LOGS","description":"Produza logs úteis com console.log(), aplique cor somente quando ela melhora a leitura e evite flood em rotinas repetitivas.","badges":["console.log","Color","Contexto","Anti-flood"],"sections":[{"id":"regra","title":"Use console.log() como saída","description":"O código da aplicação não precisa de `Logger.info()`, `Logger.warn()` ou `Logger.error()`.","blocks":[{"kind":"code","title":"logs diretos","code":"\nconsole.log('[Commands] comando registrado: ping')\nconsole.log('[Database] conexão estabelecida')\nconsole.log('[API] requisição falhou: status=502')\n          "},{"kind":"callout","tone":"success","title":"Uma convenção simples","text":"Comece com uma tag curta, descreva o evento e adicione somente os campos que ajudam a reproduzir ou medir o resultado.","icon":"✓"}]},{"id":"cores","title":"Color para ênfase visual","description":"`Color` formata texto ANSI; `console.log()` continua sendo o responsável por imprimir.","blocks":[{"kind":"code","title":"Color.ts","code":"\nimport { Color } from '@ninenity/core'\n\nconsole.log(Color.green('[Database] conectado').bold())\nconsole.log(Color.yellow('[WebSocket] reconectando'))\nconsole.log(Color.red('[API] falha ao buscar configuração').bold())\n\n// Forma funcional\nconsole.log(Color('Deploy concluído', 'lightGreen', 'bold'))\n\n// Chain vazia com text no final\nconsole.log(Color().lightBlue().bold().text('[Core] pronto'))\n          "},{"kind":"table","columns":["Cores","Estilos"],"rows":[["`red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray`","`bold`, `dim`, `italic`, `underline`"],["`lightRed`, `lightGreen`, `lightYellow`, `lightBlue`","`strikethrough`, `inverse`, `hidden`, `visible`"]]},{"kind":"callout","tone":"warning","title":"Cor não substitui contexto","text":"Um terminal inteiramente colorido é tão difícil de ler quanto um sem hierarquia. Reserve cor para estados ou tags importantes e mantenha o corpo neutro quando possível.","icon":"!"}]},{"id":"contexto","title":"Inclua contexto útil","description":"Formato consistente torna busca, dashboard e investigação muito mais simples.","blocks":[{"kind":"code","title":"contextual logging","code":"\nfunction logCommand(input: {\n  command: string\n  guildId?: string | null\n  userId: string\n  durationMs: number\n}) {\n  console.log(\n    '[Command] name=' + input.command +\n    ' guild=' + (input.guildId ?? 'dm') +\n    ' user=' + input.userId +\n    ' duration=' + input.durationMs + 'ms'\n  )\n}\n\nlogCommand({\n  command: interaction.commandName,\n  guildId: interaction.guildId,\n  userId: interaction.user.id,\n  durationMs: Date.now() - startedAt\n})\n          "},{"kind":"list","items":["Inclua o módulo ou domínio na tag: `[Prompt]`, `[Task:catalog]`, `[Database]`.","Use pares `chave=valor` para IDs, duração, quantidade e estado.","Não registre tokens, cookies, API secrets, conteúdo privado ou payloads completos de usuários.","Para erros, registre a mensagem e o ponto da operação; stack completa apenas quando realmente necessária."]}]},{"id":"anti-flood","title":"Evite flood em reconexões e loops","description":"A operação continua em frequência normal, mas a comunicação ao usuário pode ser agregada.","blocks":[{"kind":"code","title":"aviso agregado de WebSocket","code":"\nconst REPORT_INTERVAL = 10 * 60 * 1000\nlet lastReportAt = 0\nlet failedAttempts = 0\n\nfunction onWebSocketFailure(message: string) {\n  failedAttempts += 1\n  const now = Date.now()\n\n  if (lastReportAt && now - lastReportAt < REPORT_INTERVAL) return\n\n  lastReportAt = now\n  console.log(\n    '[WebSocket] conexão indisponível; reconexão segue em segundo plano' +\n    ' attempts=' + failedAttempts +\n    ' lastError=' + message\n  )\n  failedAttempts = 0\n}\n          "},{"kind":"text","text":"A função de reconexão pode continuar com backoff próprio a cada poucos segundos. O intervalo de dez minutos limita apenas o aviso visível, não a tentativa técnica."},{"kind":"code","title":"resumo de lote","code":"\nconst results = await Promise.allSettled(items.map(processItem))\nconst failed = results.filter(result => result.status === 'rejected')\n\nconsole.log(\n  '[Import] total=' + results.length +\n  ' success=' + (results.length - failed.length) +\n  ' failed=' + failed.length\n)\n          "}]},{"id":"diagnostico","title":"Diagnóstico em produção","blocks":[{"kind":"table","columns":["Sinal","Pergunta que responde"],"rows":[["Boot concluído","O auto-loader e os plugins terminaram?"],["Comandos registrados","Quantos módulos foram aceitos por guild/global?"],["Duração","A operação ficou lenta ou expirou?"],["Guild e usuário","Qual contexto reproduz o problema?"],["Tentativas agregadas","Uma integração está instável sem inundar o terminal?"]]},{"kind":"callout","tone":"info","title":"Logs contam a história, métricas mostram a tendência","text":"Use logs para eventos discretos e diagnóstico. Use telemetria para volume, latência, memória, CPU e taxa de falhas ao longo do tempo.","icon":"i"}]}],"url":"https://ninenity.vercel.app/doc/logs","markdownUrl":"https://ninenity.vercel.app/doc/markdown/logs","markdown":"# Logs e diagnóstico\n> Produza logs úteis com console.log(), aplique cor somente quando ela melhora a leitura e evite flood em rotinas repetitivas.\n\n- Área: Operação\n- URL humana: https://ninenity.vercel.app/doc/logs\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/logs\n- Pacotes e conceitos: console.log, Color, Contexto, Anti-flood\n\n## Use console.log() como saída\n\nO código da aplicação não precisa de `Logger.info()`, `Logger.warn()` ou `Logger.error()`.\n\n### logs diretos\n\n```text\n\nconsole.log('[Commands] comando registrado: ping')\nconsole.log('[Database] conexão estabelecida')\nconsole.log('[API] requisição falhou: status=502')\n```\n\n\n> **Uma convenção simples:** Comece com uma tag curta, descreva o evento e adicione somente os campos que ajudam a reproduzir ou medir o resultado.\n\n## Color para ênfase visual\n\n`Color` formata texto ANSI; `console.log()` continua sendo o responsável por imprimir.\n\n### Color.ts\n\n```text\n\nimport { Color } from '@ninenity/core'\n\nconsole.log(Color.green('[Database] conectado').bold())\nconsole.log(Color.yellow('[WebSocket] reconectando'))\nconsole.log(Color.red('[API] falha ao buscar configuração').bold())\n\n// Forma funcional\nconsole.log(Color('Deploy concluído', 'lightGreen', 'bold'))\n\n// Chain vazia com text no final\nconsole.log(Color().lightBlue().bold().text('[Core] pronto'))\n```\n\n\n| Cores | Estilos |\n| --- | --- |\n| `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray` | `bold`, `dim`, `italic`, `underline` |\n| `lightRed`, `lightGreen`, `lightYellow`, `lightBlue` | `strikethrough`, `inverse`, `hidden`, `visible` |\n\n> **Cor não substitui contexto:** Um terminal inteiramente colorido é tão difícil de ler quanto um sem hierarquia. Reserve cor para estados ou tags importantes e mantenha o corpo neutro quando possível.\n\n## Inclua contexto útil\n\nFormato consistente torna busca, dashboard e investigação muito mais simples.\n\n### contextual logging\n\n```text\n\nfunction logCommand(input: {\n  command: string\n  guildId?: string | null\n  userId: string\n  durationMs: number\n}) {\n  console.log(\n    '[Command] name=' + input.command +\n    ' guild=' + (input.guildId ?? 'dm') +\n    ' user=' + input.userId +\n    ' duration=' + input.durationMs + 'ms'\n  )\n}\n\nlogCommand({\n  command: interaction.commandName,\n  guildId: interaction.guildId,\n  userId: interaction.user.id,\n  durationMs: Date.now() - startedAt\n})\n```\n\n\n- Inclua o módulo ou domínio na tag: `[Prompt]`, `[Task:catalog]`, `[Database]`.\n- Use pares `chave=valor` para IDs, duração, quantidade e estado.\n- Não registre tokens, cookies, API secrets, conteúdo privado ou payloads completos de usuários.\n- Para erros, registre a mensagem e o ponto da operação; stack completa apenas quando realmente necessária.\n\n## Evite flood em reconexões e loops\n\nA operação continua em frequência normal, mas a comunicação ao usuário pode ser agregada.\n\n### aviso agregado de WebSocket\n\n```text\n\nconst REPORT_INTERVAL = 10 * 60 * 1000\nlet lastReportAt = 0\nlet failedAttempts = 0\n\nfunction onWebSocketFailure(message: string) {\n  failedAttempts += 1\n  const now = Date.now()\n\n  if (lastReportAt && now - lastReportAt < REPORT_INTERVAL) return\n\n  lastReportAt = now\n  console.log(\n    '[WebSocket] conexão indisponível; reconexão segue em segundo plano' +\n    ' attempts=' + failedAttempts +\n    ' lastError=' + message\n  )\n  failedAttempts = 0\n}\n```\n\n\nA função de reconexão pode continuar com backoff próprio a cada poucos segundos. O intervalo de dez minutos limita apenas o aviso visível, não a tentativa técnica.\n\n### resumo de lote\n\n```text\n\nconst results = await Promise.allSettled(items.map(processItem))\nconst failed = results.filter(result => result.status === 'rejected')\n\nconsole.log(\n  '[Import] total=' + results.length +\n  ' success=' + (results.length - failed.length) +\n  ' failed=' + failed.length\n)\n```\n\n\n## Diagnóstico em produção\n\n| Sinal | Pergunta que responde |\n| --- | --- |\n| Boot concluído | O auto-loader e os plugins terminaram? |\n| Comandos registrados | Quantos módulos foram aceitos por guild/global? |\n| Duração | A operação ficou lenta ou expirou? |\n| Guild e usuário | Qual contexto reproduz o problema? |\n| Tentativas agregadas | Uma integração está instável sem inundar o terminal? |\n\n> **Logs contam a história, métricas mostram a tendência:** Use logs para eventos discretos e diagnóstico. Use telemetria para volume, latência, memória, CPU e taxa de falhas ao longo do tempo.\n"},{"slug":"solucao-de-problemas","title":"Solução de problemas","shortTitle":"Solução de problemas","group":"Operação","eyebrow":"OPERAÇÃO / TROUBLESHOOTING","description":"Diagnostique módulos ausentes, workspaces duplicados, comandos que não aparecem e definições de prompt ou paginação não registradas.","badges":["MODULE_NOT_FOUND","Workspace","Registration"],"sections":[{"id":"modulo-nao-encontrado","title":"Cannot find module @ninenity/...","description":"O nome do import precisa combinar com o `name` do package e o workspace precisa estar mapeado pelo npm.","blocks":[{"kind":"steps","items":[{"title":"Confira o pacote","text":"Abra o `package.json` da lib e confirme que `name` usa exatamente `@ninenity/nome`."},{"title":"Confira dependências","text":"O package consumidor deve listar a lib em `dependencies` ou o monorepo precisa expor o workspace."},{"title":"Reinstale links","text":"Execute `npm install` na raiz para recriar os links simbólicos em `node_modules/@ninenity`."},{"title":"Valide exports","text":"O `main`, `types` e `exports` da lib precisam apontar para arquivos que realmente existem após o build."}]},{"kind":"code","title":"checagens na raiz","language":"powershell","code":"\nnpm query .workspace\nnpm ls @ninenity/core @ninenity/componentbuilder\nnpm run build --workspace=@ninenity/core\n          "},{"kind":"callout","tone":"warning","title":"Renomear imports não renomeia pacotes","text":"Trocar `@tefutaki/...` por `@ninenity/...` no código exige também atualizar `package.json`, lockfile e qualquer alias de TypeScript ou bundler.","icon":"!"}]},{"id":"workspace-duplicado","title":"Multiple workspaces with the same name","description":"O npm encontrou dois `package.json` com o mesmo campo `name` dentro dos padrões de workspace.","blocks":[{"kind":"code","title":"localizar nomes duplicados","language":"powershell","code":"\nGet-ChildItem -Recurse -Filter package.json |\n  Where-Object { $_.FullName -notmatch 'node_modules' } |\n  ForEach-Object {\n    $package = Get-Content -Raw $_.FullName | ConvertFrom-Json\n    [PSCustomObject]@{ Name = $package.name; Path = $_.DirectoryName }\n  } |\n  Group-Object Name |\n  Where-Object Count -gt 1\n          "},{"kind":"text","text":"Dê um nome único para cada app, mesmo que um seja apenas template. Por exemplo, `@ninenity/template-bot` e `@ninenity/example-bot`. Depois execute `npm install` na raiz para atualizar o lockfile."}]},{"id":"comando-ausente","title":"Slash funciona, prefixo não","description":"Slash e prefix são registros separados, mesmo quando compartilham o executor.","blocks":[{"kind":"list","items":["Confirme que existe uma chamada `Client.prefix()` para aquele nome.","O Client precisa do intent `GuildMessages` e, para ler texto, `MessageContent`.","Habilite Message Content Intent no Discord Developer Portal.","Confira o prefixo configurado e se o bot pode ver/enviar mensagens no canal.","Para subcomandos, confirme a sintaxe esperada pelo `PrefixCommandBuilder`."]},{"kind":"code","title":"executor compartilhado","code":"\nconst execute = async (interaction: InputCommandInteraction) => {\n  await interaction.reply('Fluxo disponível em slash e prefixo.')\n}\n\nClient.slash({\n  data: new SlashCommandBuilder().setName('prompt').setDescription('Abre o prompt.'),\n  execute\n})\n\nClient.prefix({\n  data: new PrefixCommandBuilder().setName('prompt'),\n  execute\n})\n          "}]},{"id":"prompt-nao-registrado","title":"Prompt ou paginator is not registered","description":"A solicitação chegou antes de existir uma definição com o mesmo ID no namespace do Client.","blocks":[{"kind":"table","columns":["Causa","Correção"],"rows":[["ID diferente","Compare exatamente `registerPrompt('id')` e `promptRequest('id')`"],["Arquivo fora do loader","Mova o registro para `Discord/Interactions` ou importe o módulo explicitamente"],["Arquivo não está no dist","Preserve a árvore `src/Discord` no build"],["Dois Clients","Registre a definição no mesmo Client que executa o comando"],["Exceção no módulo","Leia o primeiro erro do boot; a importação pode ter parado antes do registro"]]},{"kind":"code","title":"IDs alinhados","code":"\n// Discord/Interactions/Prompt.ts\nClient.registerPrompt('example-prompt', promptPages)\n\n// Discord/Commands/Prompt.ts\nawait interaction.promptRequest('example-prompt', { ephemeral: true })\n          "}]},{"id":"componentes-invalidos","title":"Mensagem Components V2 rejeitada","blocks":[{"kind":"list","items":["Inclua `MessageFlags.IsComponentsV2` no payload final.","Passe `template.document()` por `toDiscordMessagePayload()`.","Não aninhe containers e não misture select com outros itens na mesma action row.","Mantenha até 5 botões por linha, 10 itens por galeria e IDs interativos únicos.","Botão de link usa URL e não deve ter `customId` ou callback."]},{"kind":"code","title":"envio correto","code":"\nawait interaction.reply({\n  ...toDiscordMessagePayload(template.document()),\n  flags: MessageFlags.IsComponentsV2 | MessageFlags.Ephemeral\n})\n          "}]},{"id":"ordem-diagnostico","title":"Ordem de diagnóstico","blocks":[{"kind":"steps","items":[{"title":"Leia o primeiro erro","text":"Erros seguintes costumam ser consequência da primeira importação ou configuração inválida."},{"title":"Reduza para um módulo","text":"Teste um comando ou template mínimo para separar infraestrutura de regra de negócio."},{"title":"Valide TypeScript","text":"Execute o check do package antes de iniciar o runtime."},{"title":"Valide o build","text":"Confirme que os arquivos carregados existem na árvore compilada."},{"title":"Só então limpe cache","text":"Reinstalar tudo é último recurso; primeiro descubra qual contrato está quebrado."}]}]}],"url":"https://ninenity.vercel.app/doc/solucao-de-problemas","markdownUrl":"https://ninenity.vercel.app/doc/markdown/solucao-de-problemas","markdown":"# Solução de problemas\n> Diagnostique módulos ausentes, workspaces duplicados, comandos que não aparecem e definições de prompt ou paginação não registradas.\n\n- Área: Operação\n- URL humana: https://ninenity.vercel.app/doc/solucao-de-problemas\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/solucao-de-problemas\n- Pacotes e conceitos: MODULE_NOT_FOUND, Workspace, Registration\n\n## Cannot find module @ninenity/...\n\nO nome do import precisa combinar com o `name` do package e o workspace precisa estar mapeado pelo npm.\n\n1. **Confira o pacote** — Abra o `package.json` da lib e confirme que `name` usa exatamente `@ninenity/nome`.\n2. **Confira dependências** — O package consumidor deve listar a lib em `dependencies` ou o monorepo precisa expor o workspace.\n3. **Reinstale links** — Execute `npm install` na raiz para recriar os links simbólicos em `node_modules/@ninenity`.\n4. **Valide exports** — O `main`, `types` e `exports` da lib precisam apontar para arquivos que realmente existem após o build.\n\n### checagens na raiz\n\n```powershell\n\nnpm query .workspace\nnpm ls @ninenity/core @ninenity/componentbuilder\nnpm run build --workspace=@ninenity/core\n```\n\n\n> **Renomear imports não renomeia pacotes:** Trocar `@tefutaki/...` por `@ninenity/...` no código exige também atualizar `package.json`, lockfile e qualquer alias de TypeScript ou bundler.\n\n## Multiple workspaces with the same name\n\nO npm encontrou dois `package.json` com o mesmo campo `name` dentro dos padrões de workspace.\n\n### localizar nomes duplicados\n\n```powershell\n\nGet-ChildItem -Recurse -Filter package.json |\n  Where-Object { $_.FullName -notmatch 'node_modules' } |\n  ForEach-Object {\n    $package = Get-Content -Raw $_.FullName | ConvertFrom-Json\n    [PSCustomObject]@{ Name = $package.name; Path = $_.DirectoryName }\n  } |\n  Group-Object Name |\n  Where-Object Count -gt 1\n```\n\n\nDê um nome único para cada app, mesmo que um seja apenas template. Por exemplo, `@ninenity/template-bot` e `@ninenity/example-bot`. Depois execute `npm install` na raiz para atualizar o lockfile.\n\n## Slash funciona, prefixo não\n\nSlash e prefix são registros separados, mesmo quando compartilham o executor.\n\n- Confirme que existe uma chamada `Client.prefix()` para aquele nome.\n- O Client precisa do intent `GuildMessages` e, para ler texto, `MessageContent`.\n- Habilite Message Content Intent no Discord Developer Portal.\n- Confira o prefixo configurado e se o bot pode ver/enviar mensagens no canal.\n- Para subcomandos, confirme a sintaxe esperada pelo `PrefixCommandBuilder`.\n\n### executor compartilhado\n\n```text\n\nconst execute = async (interaction: InputCommandInteraction) => {\n  await interaction.reply('Fluxo disponível em slash e prefixo.')\n}\n\nClient.slash({\n  data: new SlashCommandBuilder().setName('prompt').setDescription('Abre o prompt.'),\n  execute\n})\n\nClient.prefix({\n  data: new PrefixCommandBuilder().setName('prompt'),\n  execute\n})\n```\n\n\n## Prompt ou paginator is not registered\n\nA solicitação chegou antes de existir uma definição com o mesmo ID no namespace do Client.\n\n| Causa | Correção |\n| --- | --- |\n| ID diferente | Compare exatamente `registerPrompt('id')` e `promptRequest('id')` |\n| Arquivo fora do loader | Mova o registro para `Discord/Interactions` ou importe o módulo explicitamente |\n| Arquivo não está no dist | Preserve a árvore `src/Discord` no build |\n| Dois Clients | Registre a definição no mesmo Client que executa o comando |\n| Exceção no módulo | Leia o primeiro erro do boot; a importação pode ter parado antes do registro |\n\n### IDs alinhados\n\n```text\n\n// Discord/Interactions/Prompt.ts\nClient.registerPrompt('example-prompt', promptPages)\n\n// Discord/Commands/Prompt.ts\nawait interaction.promptRequest('example-prompt', { ephemeral: true })\n```\n\n\n## Mensagem Components V2 rejeitada\n\n- Inclua `MessageFlags.IsComponentsV2` no payload final.\n- Passe `template.document()` por `toDiscordMessagePayload()`.\n- Não aninhe containers e não misture select com outros itens na mesma action row.\n- Mantenha até 5 botões por linha, 10 itens por galeria e IDs interativos únicos.\n- Botão de link usa URL e não deve ter `customId` ou callback.\n\n### envio correto\n\n```text\n\nawait interaction.reply({\n  ...toDiscordMessagePayload(template.document()),\n  flags: MessageFlags.IsComponentsV2 | MessageFlags.Ephemeral\n})\n```\n\n\n## Ordem de diagnóstico\n\n1. **Leia o primeiro erro** — Erros seguintes costumam ser consequência da primeira importação ou configuração inválida.\n2. **Reduza para um módulo** — Teste um comando ou template mínimo para separar infraestrutura de regra de negócio.\n3. **Valide TypeScript** — Execute o check do package antes de iniciar o runtime.\n4. **Valide o build** — Confirme que os arquivos carregados existem na árvore compilada.\n5. **Só então limpe cache** — Reinstalar tudo é último recurso; primeiro descubra qual contrato está quebrado.\n"},{"slug":"referencia","title":"Referência rápida","group":"Operação","eyebrow":"REFERÊNCIA / APIs","description":"Consulte os registradores, tipos, builders e métodos mais usados do Core e do ComponentBuilder em um único lugar.","badges":["Core API","ComponentBuilder API","Cheat sheet"],"sections":[{"id":"client-registros","title":"Registros do Client","blocks":[{"kind":"table","columns":["API","Responsabilidade"],"rows":[["`Client.slash(definition)`","Registra comando slash e deploy de application command"],["`Client.prefix(definition)`","Registra comando por prefixo, aliases, opções e grupos"],["`Client.contextMenu(definition)`","Registra menu de usuário ou mensagem"],["`Client.modal(definition)`","Registra submit handler por `customId`"],["`Client.on(event, definition)`","Executa em toda emissão do evento"],["`Client.once(event, definition)`","Executa somente na primeira emissão"],["`Client.task(definition)`","Registra rotina periódica global ou por guild"],["`Client.registerPrompt(id, definition)`","Registra prompt reutilizável ou factory"],["`Client.registerPaginator(id, definition)`","Registra páginas, Paginator ou factory"]]}]},{"id":"client-runtime","title":"Runtime e operação do Client","blocks":[{"kind":"table","columns":["API","Retorno / efeito"],"rows":[["`Client.appConfig(guildId?)`","Configuração resolvida; valores de variáveis já normalizados"],["`Client.getAppChannels(guildId?)`","Mapa de aliases para `ChannelData`"],["`Client.getAppRoles(guildId?)`","Mapa de aliases para `RoleData`"],["`Client.cooldown(duration?)`","Define ou consulta cooldown no contexto atual"],["`Client.globalCooldown(duration)`","Define cooldown global do usuário"],["`Client.queue(key, task)`","Serializa uma operação e retorna o resultado"],["`Client.runTask(id)`","Executa uma task registrada imediatamente"],["`Client.startTask(id)`","Inicia ou reativa o timer"],["`Client.stopTask(id)`","Interrompe o timer no Client atual"],["`Client.login()`","Conecta ao Discord e inicia o bootstrap"],["`Client.destroy()`","Limpa componentes, tasks, plugins e conexão"]]}]},{"id":"interacao","title":"Interação normalizada","blocks":[{"kind":"table","columns":["Propriedade / método","Uso"],"rows":[["`user`, `member`, `guild`, `guildId`, `channel`","Identidade e contexto"],["`options`","Opções de slash/prefix e subcomandos"],["`reply(payload)`","Resposta inicial"],["`deferReply(payload?)`","Reserva resposta para trabalho demorado"],["`editReply(payload)`","Edita a resposta original"],["`followUp(payload)`","Envia resposta adicional"],["`promptRequest(id, options?)`","Cria instância de prompt registrada"],["`paginatorRequest(id, options?)`","Cria instância de paginator registrada"],["`interaction`","Objeto original do Discord.js"]]},{"kind":"table","columns":["ComponentRequestOptions","Significado"],"rows":[["`pageIndex`","Página inicial, começando em 0"],["`ephemeral`","Adiciona flag de resposta efêmera"],["`flags`","Flags adicionais da mensagem"]]}]},{"id":"message-builders","title":"Builders de mensagem","blocks":[{"kind":"table","columns":["Builder","Métodos principais"],"rows":[["`MessageBuilder`","`setContent`, `addEmbeds`, `addComponents`, `setFlags`, `setAllowedMentions`, `build`, `template`"],["`ContainerBuilder`","`setAccentColor`, `setSpoiler`, `setId`, `addComponents` e aliases tipados"],["`TextDisplayBuilder`","`setContent`, `setId`"],["`SeparatorBuilder`","`setDivider`, `setSpacing`, `setId`"],["`SectionBuilder`","`setContent`, `addTextDisplayComponents`, `setAccessory`, `setButtonAccessory`, `setThumbnailAccessory`"],["`ThumbnailBuilder`","`setURL`, `setDescription`, `setSpoiler`, `setId`"],["`MediaGalleryBuilder`","`addItems`, `setId`, `build`"],["`MediaGalleryItemBuilder`","`setURL`, `setDescription`, `setSpoiler`, `build`"],["`FileBuilder`","`setURL`, `setSpoiler`, `setId`"],["`ActionRowBuilder`","`addComponents`, `setId`, `build`"]]}]},{"id":"interactive-builders","title":"Builders interativos","blocks":[{"kind":"table","columns":["Builder","Métodos principais"],"rows":[["`ButtonBuilder`","`setCustomId`, `setCallback`, `setURL`, `setLabel`, `setEmoji`, `setStyle`, `setDisabled`, `setPaginationAction`"],["`StringSelectMenuBuilder`","`setCustomId`, `setCallback`, `setPlaceholder`, `setMinValues`, `setMaxValues`, `addOptions`"],["`UserSelectMenuBuilder`","Seleção nativa de usuários + métodos comuns de select"],["`RoleSelectMenuBuilder`","Seleção nativa de cargos + métodos comuns de select"],["`ChannelSelectMenuBuilder`","Seleção nativa de canais + métodos comuns de select"],["`MentionableSelectMenuBuilder`","Seleção de usuários ou cargos + métodos comuns"],["`SelectMenuOptionBuilder`","`setLabel`, `setValue`, `setDescription`, `setEmoji`, `setDefault`"]]},{"kind":"code","title":"assinatura de callback","code":"\n.setCallback(async (interaction, id, context) => {\n  context.customId       // ID completo\n  context.values         // todos os valores de select\n  context.value          // primeiro valor, quando existe\n  context.componentType  // tipo recebido do Discord\n})\n          "}]},{"id":"template-api","title":"ComponentTemplate","blocks":[{"kind":"table","columns":["API","Uso"],"rows":[["`ComponentTemplate.create(document, metadata?)`","Cria template a partir de documento"],["`ComponentTemplate.from(input)`","Normaliza template, definição ou documento"],["`ComponentTemplate.fromJSON(json)`","Importa representação JSON"],["`template.document()`","Retorna cópia do documento"],["`template.render(context)`","Hidrata tokens com um contexto"],["`template.validate()`","Valida estrutura e limites"],["`template.toJSON()`","Exporta representação JSON"],["`template.toToken()`","Gera token assinado quando configurado"],["`template.toUnsignedToken()`","Gera token sem assinatura para fluxo local"],["`template.toTypeScript()`","Exporta uma representação TypeScript"]]}]},{"id":"paginator-api","title":"Paginator e Prompt","blocks":[{"kind":"table","columns":["API","Uso"],"rows":[["`Paginator.static({ pages })`","Páginas prontas"],["`Paginator.dynamic({ data, pageSize, render })`","Divide dados e renderiza cada fatia"],["`Paginator.hybrid({ intro, data, pageSize, render })`","Introdução fixa seguida de dados"],["`paginator.state(pageIndex?)`","Estado normalizado da página"],["`paginator.render(pageIndex?)`","Template da página"],["`paginator.renderDocument(pageIndex?)`","Documento pronto"],["`Prompt.create(options)`","Prompt a partir de template e opções"],["`Prompt.fromPages(pages, options?)`","Prompt roteado por páginas exportadas"],["`prompt.render()`","Template da pergunta"],["`prompt.resolveOption(customId)`","Resolve a opção pelo ID completo"],["`prompt.resolvePage(optionId)`","Encontra página associada à opção"]]}]},{"id":"imports-rapidos","title":"Imports rápidos","blocks":[{"kind":"code","title":"core-imports.ts","code":"\nimport {\n  Ninenity,\n  LibsClient,\n  Color,\n  I18n,\n  SlashCommandBuilder,\n  PrefixCommandBuilder,\n  ContextMenuCommandBuilder,\n  GatewayIntentBits,\n  Events,\n  MessageFlags,\n  type AppConfig,\n  type InputCommandInteraction\n} from '@ninenity/core'\n          "},{"kind":"code","title":"component-imports.ts","code":"\nimport {\n  MessageBuilder,\n  ContainerBuilder,\n  TextDisplayBuilder,\n  SeparatorBuilder,\n  SectionBuilder,\n  ThumbnailBuilder,\n  MediaGalleryBuilder,\n  FileBuilder,\n  ActionRowBuilder,\n  ButtonBuilder,\n  StringSelectMenuBuilder,\n  ButtonStyle,\n  ComponentTemplate,\n  Paginator,\n  Prompt,\n  toDiscordMessagePayload\n} from '@ninenity/componentbuilder'\n          "}]}],"url":"https://ninenity.vercel.app/doc/referencia","markdownUrl":"https://ninenity.vercel.app/doc/markdown/referencia","markdown":"# Referência rápida\n> Consulte os registradores, tipos, builders e métodos mais usados do Core e do ComponentBuilder em um único lugar.\n\n- Área: Operação\n- URL humana: https://ninenity.vercel.app/doc/referencia\n- URL Markdown: https://ninenity.vercel.app/doc/markdown/referencia\n- Pacotes e conceitos: Core API, ComponentBuilder API, Cheat sheet\n\n## Registros do Client\n\n| API | Responsabilidade |\n| --- | --- |\n| `Client.slash(definition)` | Registra comando slash e deploy de application command |\n| `Client.prefix(definition)` | Registra comando por prefixo, aliases, opções e grupos |\n| `Client.contextMenu(definition)` | Registra menu de usuário ou mensagem |\n| `Client.modal(definition)` | Registra submit handler por `customId` |\n| `Client.on(event, definition)` | Executa em toda emissão do evento |\n| `Client.once(event, definition)` | Executa somente na primeira emissão |\n| `Client.task(definition)` | Registra rotina periódica global ou por guild |\n| `Client.registerPrompt(id, definition)` | Registra prompt reutilizável ou factory |\n| `Client.registerPaginator(id, definition)` | Registra páginas, Paginator ou factory |\n\n## Runtime e operação do Client\n\n| API | Retorno / efeito |\n| --- | --- |\n| `Client.appConfig(guildId?)` | Configuração resolvida; valores de variáveis já normalizados |\n| `Client.getAppChannels(guildId?)` | Mapa de aliases para `ChannelData` |\n| `Client.getAppRoles(guildId?)` | Mapa de aliases para `RoleData` |\n| `Client.cooldown(duration?)` | Define ou consulta cooldown no contexto atual |\n| `Client.globalCooldown(duration)` | Define cooldown global do usuário |\n| `Client.queue(key, task)` | Serializa uma operação e retorna o resultado |\n| `Client.runTask(id)` | Executa uma task registrada imediatamente |\n| `Client.startTask(id)` | Inicia ou reativa o timer |\n| `Client.stopTask(id)` | Interrompe o timer no Client atual |\n| `Client.login()` | Conecta ao Discord e inicia o bootstrap |\n| `Client.destroy()` | Limpa componentes, tasks, plugins e conexão |\n\n## Interação normalizada\n\n| Propriedade / método | Uso |\n| --- | --- |\n| `user`, `member`, `guild`, `guildId`, `channel` | Identidade e contexto |\n| `options` | Opções de slash/prefix e subcomandos |\n| `reply(payload)` | Resposta inicial |\n| `deferReply(payload?)` | Reserva resposta para trabalho demorado |\n| `editReply(payload)` | Edita a resposta original |\n| `followUp(payload)` | Envia resposta adicional |\n| `promptRequest(id, options?)` | Cria instância de prompt registrada |\n| `paginatorRequest(id, options?)` | Cria instância de paginator registrada |\n| `interaction` | Objeto original do Discord.js |\n\n| ComponentRequestOptions | Significado |\n| --- | --- |\n| `pageIndex` | Página inicial, começando em 0 |\n| `ephemeral` | Adiciona flag de resposta efêmera |\n| `flags` | Flags adicionais da mensagem |\n\n## Builders de mensagem\n\n| Builder | Métodos principais |\n| --- | --- |\n| `MessageBuilder` | `setContent`, `addEmbeds`, `addComponents`, `setFlags`, `setAllowedMentions`, `build`, `template` |\n| `ContainerBuilder` | `setAccentColor`, `setSpoiler`, `setId`, `addComponents` e aliases tipados |\n| `TextDisplayBuilder` | `setContent`, `setId` |\n| `SeparatorBuilder` | `setDivider`, `setSpacing`, `setId` |\n| `SectionBuilder` | `setContent`, `addTextDisplayComponents`, `setAccessory`, `setButtonAccessory`, `setThumbnailAccessory` |\n| `ThumbnailBuilder` | `setURL`, `setDescription`, `setSpoiler`, `setId` |\n| `MediaGalleryBuilder` | `addItems`, `setId`, `build` |\n| `MediaGalleryItemBuilder` | `setURL`, `setDescription`, `setSpoiler`, `build` |\n| `FileBuilder` | `setURL`, `setSpoiler`, `setId` |\n| `ActionRowBuilder` | `addComponents`, `setId`, `build` |\n\n## Builders interativos\n\n| Builder | Métodos principais |\n| --- | --- |\n| `ButtonBuilder` | `setCustomId`, `setCallback`, `setURL`, `setLabel`, `setEmoji`, `setStyle`, `setDisabled`, `setPaginationAction` |\n| `StringSelectMenuBuilder` | `setCustomId`, `setCallback`, `setPlaceholder`, `setMinValues`, `setMaxValues`, `addOptions` |\n| `UserSelectMenuBuilder` | Seleção nativa de usuários + métodos comuns de select |\n| `RoleSelectMenuBuilder` | Seleção nativa de cargos + métodos comuns de select |\n| `ChannelSelectMenuBuilder` | Seleção nativa de canais + métodos comuns de select |\n| `MentionableSelectMenuBuilder` | Seleção de usuários ou cargos + métodos comuns |\n| `SelectMenuOptionBuilder` | `setLabel`, `setValue`, `setDescription`, `setEmoji`, `setDefault` |\n\n### assinatura de callback\n\n```text\n\n.setCallback(async (interaction, id, context) => {\n  context.customId       // ID completo\n  context.values         // todos os valores de select\n  context.value          // primeiro valor, quando existe\n  context.componentType  // tipo recebido do Discord\n})\n```\n\n\n## ComponentTemplate\n\n| API | Uso |\n| --- | --- |\n| `ComponentTemplate.create(document, metadata?)` | Cria template a partir de documento |\n| `ComponentTemplate.from(input)` | Normaliza template, definição ou documento |\n| `ComponentTemplate.fromJSON(json)` | Importa representação JSON |\n| `template.document()` | Retorna cópia do documento |\n| `template.render(context)` | Hidrata tokens com um contexto |\n| `template.validate()` | Valida estrutura e limites |\n| `template.toJSON()` | Exporta representação JSON |\n| `template.toToken()` | Gera token assinado quando configurado |\n| `template.toUnsignedToken()` | Gera token sem assinatura para fluxo local |\n| `template.toTypeScript()` | Exporta uma representação TypeScript |\n\n## Paginator e Prompt\n\n| API | Uso |\n| --- | --- |\n| `Paginator.static({ pages })` | Páginas prontas |\n| `Paginator.dynamic({ data, pageSize, render })` | Divide dados e renderiza cada fatia |\n| `Paginator.hybrid({ intro, data, pageSize, render })` | Introdução fixa seguida de dados |\n| `paginator.state(pageIndex?)` | Estado normalizado da página |\n| `paginator.render(pageIndex?)` | Template da página |\n| `paginator.renderDocument(pageIndex?)` | Documento pronto |\n| `Prompt.create(options)` | Prompt a partir de template e opções |\n| `Prompt.fromPages(pages, options?)` | Prompt roteado por páginas exportadas |\n| `prompt.render()` | Template da pergunta |\n| `prompt.resolveOption(customId)` | Resolve a opção pelo ID completo |\n| `prompt.resolvePage(optionId)` | Encontra página associada à opção |\n\n## Imports rápidos\n\n### core-imports.ts\n\n```text\n\nimport {\n  Ninenity,\n  LibsClient,\n  Color,\n  I18n,\n  SlashCommandBuilder,\n  PrefixCommandBuilder,\n  ContextMenuCommandBuilder,\n  GatewayIntentBits,\n  Events,\n  MessageFlags,\n  type AppConfig,\n  type InputCommandInteraction\n} from '@ninenity/core'\n```\n\n\n### component-imports.ts\n\n```text\n\nimport {\n  MessageBuilder,\n  ContainerBuilder,\n  TextDisplayBuilder,\n  SeparatorBuilder,\n  SectionBuilder,\n  ThumbnailBuilder,\n  MediaGalleryBuilder,\n  FileBuilder,\n  ActionRowBuilder,\n  ButtonBuilder,\n  StringSelectMenuBuilder,\n  ButtonStyle,\n  ComponentTemplate,\n  Paginator,\n  Prompt,\n  toDiscordMessagePayload\n} from '@ninenity/componentbuilder'\n```\n"}]}