Spraxium logoSpraxium

Embeds

Monte embeds ricos usando um sistema declarativo guiado por schema e decorators. Aprenda definição de campos, renderização condicional, propriedades dinâmicas, o DescriptionBuilder e a renderização com EmbedService.

Embeds declarativos

O sistema de embeds em @spraxium/components permite definir embeds do Discord como classes decoradas, em vez de encadear builders de forma imperativa. Cada propriedade do embed, como título, cor, campos, footer e imagens, é descrita por decorators aplicados nas propriedades da classe. Na renderização, o EmbedService lê esses metadados e monta um EmbedBuilder padrão do discord.js com todos os valores declarados.

Essa abordagem entrega os mesmos ganhos vistos em botões e selects: as definições ficam reaproveitáveis, os campos podem ser tratados isoladamente e a renderização continua separada da regra de negócio. Embeds dirigidos por dados ficam especialmente fortes aqui, porque cada decorator pode receber uma factory function que calcula o valor no momento da renderização com base no objeto de dados informado.

O decorator @Embed marca a classe como um schema de embed. Ele pode receber um objeto estático EmbedSchema com propriedades de topo, como title, description, color, footer e url. Quando a propriedade precisa depender de dados de runtime, o caminho certo é usar decorators como @EmbedTitle, @EmbedColor e @EmbedField.

Embed básico com propriedades estáticas

O caso mais simples usa o decorator de classe @Embed com um schema estático. Esse formato funciona bem quando o conteúdo do embed não muda em runtime. O objeto de schema aceita os mesmos campos que a API de EmbedBuilder do discord.js: title, description, color, footer, timestamp, author, thumbnail, image e url.

src/components/embeds/welcome.embed.ts
import { Embed } from '@spraxium/components';

@Embed({
  title: 'Welcome',
  description: 'Welcome to the server! Please read the rules.',
  color: 0x5865f2,
  footer: { text: 'Spraxium Bot' },
  timestamp: true,
})
export class WelcomeEmbed {}

Para renderizar um embed estático, injete EmbedService e chame build com a classe. O serviço lê o metadado gerado por @Embed e devolve o EmbedBuilder já configurado.

Embeds orientados por dados com decorators

Quando o embed depende de dados de runtime, o caminho é usar decorators nas propriedades. Cada decorator aceita um valor estático ou uma factory function que recebe o objeto de dados. Com isso, você consegue calcular título, cor, campos e outras propriedades dinamicamente, sem espalhar a definição por vários pontos do código.

src/components/embeds/user-profile.embed.ts
import {
  Embed,
  EmbedTitle,
  EmbedColor,
  EmbedField,
  EmbedThumbnail,
  EmbedFooter,
} from '@spraxium/components';

interface ProfileData {
  username: string;
  level: number;
  isPremium: boolean;
  avatarUrl: string;
}

@Embed()
export class UserProfileEmbed {
  @EmbedTitle<ProfileData>((data) => `${data.username}'s Profile`)
  title!: never;

  @EmbedColor<ProfileData>((data) => (data.isPremium ? 0xffd700 : 0x5865f2))
  color!: never;

  @EmbedThumbnail<ProfileData>((data) => data.avatarUrl)
  thumbnail!: never;

  @EmbedField<ProfileData>({
    name: 'Level',
    value: (data) => String(data.level),
    inline: true,
  })
  level!: never;

  @EmbedField<ProfileData>({
    name: 'Status',
    value: (data) => (data.isPremium ? '⭐ Premium' : 'Free'),
    inline: true,
  })
  status!: never;

  @EmbedFooter<ProfileData>((data) => ({ text: `ID: ${data.username}` }))
  footer!: never;
}

O parâmetro genérico em cada decorator dá segurança de tipo para o argumento da factory function. A declaração da propriedade com !: never é apenas uma convenção de TypeScript para satisfazer o compilador sem criar estado real na instância, já que os valores do embed vêm dos metadados e não de propriedades armazenadas.

A factory de @EmbedDescription pode retornar uma string comum ou uma instância de DescriptionBuilder. O atalho desc() cria esse builder encadeável, permitindo montar descrições estruturadas sem depender de concatenação de string ou template literals desnecessariamente barulhentos.

src/components/embeds/account-summary.embed.ts
import {
  Embed,
  EmbedTitle,
  EmbedDescription,
  EmbedColor,
  desc,
} from '@spraxium/components';

interface AccountData {
  username: string;
  balance: number;
  isPremium: boolean;
}

@Embed()
export class AccountSummaryEmbed {
  @EmbedTitle<AccountData>((data) => `${data.username}'s Account`)
  title!: never;

  @EmbedDescription<AccountData>((data) =>
    desc()
      .bold(`Balance: ${data.balance} coins`)
      .empty()
      .when(data.isPremium, (b) =>
        b.quote("\u2b50 Premium \u2014 2\u00d7 rewards active"),
      )
      .build(),
  )
  description!: never;

  @EmbedColor<AccountData>((data) => (data.isPremium ? 0xffd700 : 0x5865f2))
  color!: never;
}

Decorators de propriedade do embed

Cada decorator de propriedade controla um aspecto específico da aparência do embed. Todos aceitam um valor estático ou uma factory function que recebe o objeto de dados no momento da renderização.

DecoradorTipo de valorFinalidade
@EmbedTitleString ou função que retorna stringDefine o título do embed. A versão com factory permite personalizar cada renderização.
@EmbedDescriptionString ou função que retorna stringSubstitui o campo description do schema @Embed por um valor dinâmico.
@EmbedColorNúmero, string, ou função que retorna um dos doisCor em hexadecimal ou nomeada. A factory permite cores condicionais.
@EmbedFooterString, FooterConfig, ou função que retorna um dos doisTexto do rodapé e URL opcional de ícone.
@EmbedTimestampBoolean, Date, ou função que retorna um dos doisQuando está como true, usa o horário atual. Também aceita Date para timestamps específicos.
@EmbedAuthorAuthorConfig ou função que o retornaNome do autor, URL de ícone e link opcional.
@EmbedThumbnailString ou função que retorna stringImagem pequena exibida no canto superior direito.
@EmbedImageString ou função que retorna stringImagem principal exibida na parte inferior do embed.
@EmbedUrlString ou função que retorna stringTransforma o título do embed em um link clicável.

Campos do embed

O decorator @EmbedField registra um campo de nome e valor no embed. Esses campos aparecem como linhas estruturadas abaixo da descrição. A propriedade value pode ser uma string estática ou uma factory function para conteúdo dinâmico. Já inline controla se o campo divide a mesma linha com campos vizinhos também marcados como inline.

src/components/embeds/server-stats.embed.ts
import { Embed, EmbedTitle, EmbedField } from '@spraxium/components';

interface StatsData {
  memberCount: number;
  onlineCount: number;
  boostLevel: number;
}

@Embed({ color: 0x00bfff })
export class ServerStatsEmbed {
  @EmbedTitle('Server Statistics')
  title!: never;

  @EmbedField<StatsData>({
    name: 'Members',
    value: (d) => String(d.memberCount),
    inline: true,
  })
  members!: never;

  @EmbedField<StatsData>({
    name: 'Online',
    value: (d) => String(d.onlineCount),
    inline: true,
  })
  online!: never;

  @EmbedField<StatsData>({
    name: 'Boost Level',
    value: (d) => `Level ${d.boostLevel}`,
    inline: true,
  })
  boost!: never;
}

Campos condicionais

O decorator @EmbedWhen inclui um campo de forma condicional com base em uma função predicado. Essa função recebe o objeto de dados e retorna um booleano. Quando o retorno é false, o campo simplesmente não entra no embed renderizado. Isso é útil para mostrar campos apenas em certos papéis, planos ou estados específicos dos dados.

src/components/embeds/premium-profile.embed.ts
import { Embed, EmbedField, EmbedWhen } from '@spraxium/components';

interface ProfileData {
  username: string;
  isPremium: boolean;
  premiumSince?: string;
}

@Embed({ title: 'Profile' })
export class PremiumProfileEmbed {
  @EmbedField<ProfileData>({ name: 'User', value: (d) => d.username })
  user!: never;

  @EmbedWhen<ProfileData>((d) => d.isPremium)
  @EmbedField<ProfileData>({
    name: 'Premium Since',
    value: (d) => d.premiumSince ?? 'Unknown',
  })
  premiumField!: never;
}

Aplique sempre @EmbedWhen antes de @EmbedField na mesma propriedade. A ordem de empilhamento importa, porque, se o predicado retornar false, o metadado do campo é pulado por completo durante a renderização.

DescriptionBuilder

DescriptionBuilder oferece uma API fluente para compor descrições em Markdown com mais estrutura. Ele sabe lidar com níveis de heading, texto em negrito, itálico, blocos de código, listas e quebras de linha. Isso é especialmente útil quando a descrição do embed precisa de formatação mais rica e você não quer cair em concatenação manual de string o tempo todo.

src/utils/build-help-description.ts
import { DescriptionBuilder } from '@spraxium/components';

const description = new DescriptionBuilder()
  .heading('Commands', 2)
  .bold('!help')
  .text(' - Shows this message')
  .newline()
  .bold('!ping')
  .text(' - Check bot latency')
  .newline()
  .heading('Links', 2)
  .link('Documentation', 'https://docs.example.com')
  .build();

Renderização com EmbedService

EmbedService é o serviço injetável que transforma classes de embed decoradas em objetos EmbedBuilder do discord.js. O método build recebe a classe e, opcionalmente, um objeto de dados. Além disso, o serviço oferece atalhos para alguns padrões comuns de embed.

MétodoFinalidade
build

Renderiza uma classe de embed decorada em um EmbedBuilder com os dados informados.

simple

Cria um embed simples com descrição e cor opcional, sem precisar de classe dedicada.

error

Cria um embed de erro com tema vermelho e uma descrição.

src/commands/profile.command-handler.ts
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import { EmbedService } from '@spraxium/components';
import type { ChatInputCommandInteraction } from 'discord.js';
import { UserProfileEmbed } from '../components/embeds/user-profile.embed';
import { ProfileCommand } from './profile.command';

@SlashCommandHandler(ProfileCommand)
export class ProfileCommandHandler {
  constructor(private readonly embeds: EmbedService) {}

  async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
    const profileData = {
      username: interaction.user.username,
      level: 42,
      isPremium: true,
      avatarUrl: interaction.user.displayAvatarURL(),
    };

    const embed = this.embeds.build(UserProfileEmbed, profileData);
    await interaction.reply({ embeds: [embed] });
  }
}

Registro no módulo

Classes de embed não exigem registro em módulo. EmbedService é fornecido globalmente por ComponentsModule e pode ser injetado em qualquer command handler sem configuração adicional. Você passa a classe do embed diretamente para embeds.build() no momento da chamada. O serviço lê os metadados direto do construtor da classe, não da DI. O módulo da feature só precisa listar comandos e handlers que usam esse serviço.

src/modules/profile/profile.module.ts
import { Module } from '@spraxium/common';
import { ProfileCommand } from './commands/profile.command';

@Module({
  commands: [ProfileCommand],
})
export class ProfileModule {}