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.
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.
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.
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.
| Decorador | Tipo de valor | Finalidade |
|---|---|---|
| @EmbedTitle | String ou função que retorna string | Define o título do embed. A versão com factory permite personalizar cada renderização. |
| @EmbedDescription | String ou função que retorna string | Substitui o campo description do schema @Embed por um valor dinâmico. |
| @EmbedColor | Número, string, ou função que retorna um dos dois | Cor em hexadecimal ou nomeada. A factory permite cores condicionais. |
| @EmbedFooter | String, FooterConfig, ou função que retorna um dos dois | Texto do rodapé e URL opcional de ícone. |
| @EmbedTimestamp | Boolean, Date, ou função que retorna um dos dois | Quando está como true, usa o horário atual. Também aceita Date para timestamps específicos. |
| @EmbedAuthor | AuthorConfig ou função que o retorna | Nome do autor, URL de ícone e link opcional. |
| @EmbedThumbnail | String ou função que retorna string | Imagem pequena exibida no canto superior direito. |
| @EmbedImage | String ou função que retorna string | Imagem principal exibida na parte inferior do embed. |
| @EmbedUrl | String ou função que retorna string | Transforma 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.
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.
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.
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étodo | Finalidade |
|---|---|
| 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. |
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.
import { Module } from '@spraxium/common';
import { ProfileCommand } from './commands/profile.command';
@Module({
commands: [ProfileCommand],
})
export class ProfileModule {}