# Components

Build the cards, buttons, and rows your bot sends as classes over the discord.js builders. Covers a row of buttons, which builder each key wraps, your bot color, and emojis by name.

A line of text needs nothing more than `this.reply('Banned.')`. A card with a heading, a body, and a row of buttons under it needs a discord.js builder. You put that builder in a class.

Subclass [`BuilderComponent`](https://docs.seedcord.org/packages/core/latest/builder-component), name the builder you want in the generic and in the `super()` call, and build the thing in the constructor.

```ts title="src/components/BanCard.ts"
import { BuilderComponent } from '@seedcord/gateway';

export class BanCard extends BuilderComponent<'container'> {
    constructor(name: string, reason: string) {
        super('container');

        this.instance.addTextDisplayComponents(
            (text) => text.setContent(`## Banned ${name}`),
            (text) => text.setContent(reason)
        );
    }
}
```

`'container'` appears twice up there, once in the generic and once in the `super()` call. The `super()` argument picks which builder gets constructed. The generic types `this.instance` as that builder. A `ContainerBuilder` declares no `setTitle`, so calling one stops the build.

Write the two the same. The compiler rejects a mismatch.

`name` and `reason` arrive through the constructor, so one `BanCard` covers every ban card your bot sends. Your ban handler passes its own values. The next handler that needs one imports the same file.

To reword the card, you change one line in `BanCard.ts`. Every handler that sends one gets the new wording.

`this.instance` stays protected, so your handler reads `.component` to get the builder out.

```ts title="src/handlers/Ban.ts"
@SlashRoute('ban')
export class Ban extends SlashHandler<'ban'> {
    public async execute(): Promise<void> {
        const target = this.options.getUser('target');
        const reason = this.options.getString('reason');

        const card = new BanCard(target.username, reason ?? 'none');
        await this.reply({ components: [card.component] });
    }
}
```

`card.component` there is the live builder, so a setter you call on it does change the message. Nothing stops you.

Make that call in the constructor anyway. `this.instance` is protected to keep a ban card's whole description in one file. A call from your handler splits that description across two. Then, if you go to `BanCard.ts`, you no longer see the complete card.

Reading `.component` also applies your [bot color](#your-bot-color). The color resolves on that read. Resolving it in the constructor would give the default color to every component built before your config loads.

> **Tip**
>
> seedcord imports your commands, handlers, events, and subscribers from the paths you set in your bot config. Your own code imports a component directly, so your config never mentions that folder. Make one anyway, at `src/components`, and think in building blocks.
>
> A card is a building block, and a row of buttons is another. A modal is a building block, and a row of inputs is another, maybe you have a menu builder that dynamically creates its values, or you want to create items in one conditionally. Each one is a `BuilderComponent` subclass in that folder. Your handlers import the ones they send.
>
> For example, you put a `BanCard` and a `BanActions` row of buttons in that folder, and your `Ban` handler imports both.
>
> ```txt title="my-bot" output
> src/
> ├─ commands/
> ├─ components/
> │  ├─ BanCard.ts
> │  └─ BanActions.ts
> ├─ handlers/
> └─ bot.ts
> ```

## A row of buttons

`ActionRowBuilder` is generic over what goes inside it, so a row you build by hand writes the child builder into the type every time. [`RowComponent`](https://docs.seedcord.org/packages/core/latest/row-component) takes a key and does that for you, typing `this.instance` to whatever the row holds. A row holds buttons or one select menu.

```ts title="src/components/BanActions.ts"
export const AppealId = new CustomId('appeal').snowflake('userId');

export class BanActions extends RowComponent<'button'> {
    constructor(userId: string) {
        super('button');

        this.instance.addComponents(
            new ButtonBuilder()
                .setCustomId(AppealId.encode({ userId }))
                .setLabel('Appeal')
                .setStyle(ButtonStyle.Secondary)
        );
    }
}
```

`AppealId` sits outside the class, exported. The button encodes a `userId` into it. Discord hands that same string back when someone clicks, and your handler imports `AppealId` to decode it.

Keep that declaration in the file with the component that encodes it. Both sides read the field names off one [`CustomId`](https://docs.seedcord.org/packages/custom-id/latest/custom-id), so a rename cannot reach one and miss the other.

`addActionRowComponents` puts a row inside a container, which is how a card and its buttons reach Discord as one message.

> **Warning**
>
> Import every builder from `@discordjs/builders`. discord.js re-exports its own CJS copy of each one, so the two copies are always different classes. Nesting one inside the other is where that shows, because `instanceof` returns false and `toJSON` misbehaves.
>
> The `@seedcord/no-djs-builder-import` lint rule flags every runtime import of a builder from `discord.js`. A type-only import is erased before runtime so that one is safe.

## Which builder each key wraps

Both the generic and the `super()` call take one of these keys.

{/* prettier-ignore-start */}

| what you are building           | keys                                                                                                                                      |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| a message, ComponentsV2         | `container`, `text_display`, `section`, `separator`, `media`, `file`                                                                      |
| an embed                        | `embed`                                                                                                                                   |
| a command definition            | `command`, `context_menu`, `subcommand`, `group`                                                                                          |
| a modal                         | `modal`, `label`, `text_input`, `file_upload`, `checkbox`, `checkbox_group`, `checkbox_group_option`, `radio_group`, `radio_group_option` |
| a row child                     | `button`, `menu_string`, `menu_user`, `menu_role`, `menu_channel`, `menu_mentionable`                                                     |
| one option inside a string menu | `menu_option_string`                                                                                                                      |

{/* prettier-ignore-end */}

`BuilderComponent` takes every key in that table. The six row-child keys also work with `RowComponent`, which wraps each one in an `ActionRowBuilder`. So `RowComponent<'button'>` types `this.instance` as a row of buttons. `BuilderComponent<'button'>` types it as the bare `ButtonBuilder` you would add to one.

## Your bot color

Every embed and container your bot sends takes a color. Setting it on each builder leaves every new component one more place to forget it.

`botColor` takes one value for the whole bot.

```ts title="src/bot.ts"
import { resolve } from 'node:path';

import { Seedcord } from '@seedcord/gateway';
import { GatewayIntentBits } from 'discord.js';

export const seedcord = new Seedcord({
    bot,
    subscribers: { path: null },
    botColor: '#fe565a'
});
```

Reading `.component` applies it. An embed takes it as its color, and a container takes it as its accent. Every other builder gets nothing, since nothing else carries one.

Set your own in the constructor, with `setColor` on an embed or `setAccentColor` on a container. The check only fills an empty color, so yours survives.

```ts title="src/components/MaintenanceEmbed.ts"
export class MaintenanceEmbed extends BuilderComponent<'embed'> {
    constructor(until: string) {
        super('embed');

        this.instance
            .setTitle('Maintenance')
            .setDescription(`Back at ${until}.`);
    }
}
```

Discord forbids an embed on a ComponentsV2 message, which is what seedcord's reply methods send. An embed reaches Discord through a channel send or a [raw ack](/replying/raw-acks).

`botColor` accepts a `#hex` string, a discord.js color name like `'Blurple'`, a raw integer, or an `[r, g, b]` tuple. Leave the key out for Discord's default.

## Emojis by name

You've gone hunting for an emoji id before. Copy `<:streak_flame:1872389747982323426>` out of the client, paste it into a constants file, then do the next one. From then on you keep that file in sync by hand.

A re-upload gives the emoji a new id, so every copy of the old one points at nothing. A rename leaves the name in your code out of step with the one in Discord. Some emojis come from your app and some from one guild, so each of those is its own lookup.

seedcord does those lookups for you, once, while the bot starts. Name each custom emoji in your config, then read it back by that name.

```ts title="src/bot.ts"
export const seedcord = new Seedcord({
    bot: {
        ...shared,
        events: { path: null },
        emojis: {
            Confirm: 'confirmation_check',
            Cancel: 'confirmation_cross',
            // or look up an emoji in a guild your bot is in
            Streak: ['streak_flame', '1872389747982323426']
        }
    },
    subscribers: { path: null }
});
```

A plain string is the name of an application emoji, one you upload to your app in the developer portal. Run `seedcord codegen` after you change that block. It writes the key names into [`EmojiMap`](https://docs.seedcord.org/packages/types/latest/emoji-map), which types the [`Emojis`](https://docs.seedcord.org/packages/gateway/latest/emojis) accessor.

Your editor then offers those keys the moment you type the dot.

```ts
import { Emojis } from '@seedcord/gateway';

Emojis.
```

```ts title="src/components/StreakCard.ts"
export class StreakCard extends BuilderComponent<'container'> {
    constructor(days: number) {
        super('container');

        this.instance.addTextDisplayComponents((text) =>
            text.setContent(`${Emojis.Streak} ${days} days`)
        );
    }
}
```

A resolved emoji stringifies to `<:name:id>`, or `<a:name:id>` when it animates. It also passes straight to any builder's `setEmoji`.

```ts
this.instance.addComponents(
    new ButtonBuilder()
        .setCustomId(confirmId)
        .setEmoji(Emojis.Confirm)
        .setLabel('Confirm')
        .setStyle(ButtonStyle.Success)
);
```

seedcord stops startup when a name won't resolve, and the error carries every failure at once. `Emojis` throws when you read a key before startup fills it.

> **Gateway only**
>
> A `[name, guildId]` emoji needs the `Guilds` intent, since seedcord reads that guild's emoji list off the discord.js client cache. Without it startup reports the guild as unavailable. On http the same tuple resolves over one REST call to that guild's emoji route.

> **Gateway and http differ**
>
> On gateway, each one is a [`GatewayEmoji`](https://docs.seedcord.org/packages/gateway/latest/gateway-emoji). In the example above, `Emojis.Streak` carries a `.source` holding the live discord.js `GuildEmoji` or `ApplicationEmoji`, typed per key from the tag codegen wrote. On http it is a plain [`ResolvedEmoji`](https://docs.seedcord.org/packages/core/latest/resolved-emoji) with the name, the id, and the animated flag. Both stringify and both pass to `setEmoji`.
