@lunibee/builders 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,6 +55,13 @@ console.log(embed.toJSON(), buttons.toJSON(), menu.toJSON(), ping.toJSON());
55
55
 
56
56
  Every builder validates Discord's limits as you set values and serialises with `toJSON()`.
57
57
 
58
+ ## What's new in 0.2.1
59
+
60
+ - `ModalBuilder.addTextInputs(...inputs)` wraps each text input in its own row.
61
+ - `componentsV2Message(components, options?)` builds a Components V2 message with the required flag.
62
+ - `EmbedBuilder` setters (`setTitle`, `setDescription`, `setURL`, `setThumbnail`, `setImage`) clear the field with `null`.
63
+ - `TextInputStyle` and `ComponentType` are usable as types; `ModalBuilder.toJSON()` is typed with a required `custom_id` and `title`.
64
+
58
65
  ## What's new in 0.2.0
59
66
 
60
67
  `toJSON()` validates whole components (required fields, value bounds, action-row rules, button field exclusivity); `ButtonBuilder.setSKUId()` for premium buttons.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunibee/builders",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "module": "./src/index.ts",
6
6
  "types": "./src/index.ts",
package/src/components.ts CHANGED
@@ -23,6 +23,11 @@ export const ComponentType = {
23
23
  Container: 17,
24
24
  } as const;
25
25
  export const TextInputStyle = { Short: 1, Paragraph: 2 } as const;
26
+ /** One {@link TextInputStyle} value, usable as a type. */
27
+ export type TextInputStyle =
28
+ (typeof TextInputStyle)[keyof typeof TextInputStyle];
29
+ /** One {@link ComponentType} value, usable as a type. */
30
+ export type ComponentType = (typeof ComponentType)[keyof typeof ComponentType];
26
31
  export interface APIComponentEmoji {
27
32
  id?: string | null;
28
33
  name?: string | null;
@@ -83,8 +88,8 @@ export interface APITextInputComponent {
83
88
  }
84
89
  export interface APIModalComponent {
85
90
  type: 9;
86
- custom_id?: string;
87
- title?: string;
91
+ custom_id: string;
92
+ title: string;
88
93
  components: APIActionRowComponent[];
89
94
  }
90
95
  export type APIActionRowChild =
@@ -435,6 +440,14 @@ export class ModalBuilder {
435
440
  this.#components.push(...components);
436
441
  return this;
437
442
  }
443
+ /** Adds text inputs, each in its own action row (Discord's modal layout). @param inputs Text inputs, at most 5 per modal. */
444
+ public addTextInputs(...inputs: TextInputBuilder[]): this {
445
+ return this.addComponents(
446
+ ...inputs.map((input) =>
447
+ new ActionRowBuilder<TextInputBuilder>().addComponents(input),
448
+ ),
449
+ );
450
+ }
438
451
  public toJSON(): APIModalComponent {
439
452
  if (!this.#custom_id || !this.#title)
440
453
  throw new TypeError("Modals require a custom ID and a title.");
@@ -442,8 +455,8 @@ export class ModalBuilder {
442
455
  throw new RangeError("Modals require at least one component.");
443
456
  return {
444
457
  type: 9,
445
- ...(this.#custom_id ? { custom_id: this.#custom_id } : {}),
446
- ...(this.#title ? { title: this.#title } : {}),
458
+ custom_id: this.#custom_id,
459
+ title: this.#title,
447
460
  components: this.#components.map((component) => component.toJSON()),
448
461
  };
449
462
  }
@@ -757,3 +770,31 @@ function assertSelect(data: {
757
770
  if ((data.min_values ?? 1) > (data.max_values ?? 1))
758
771
  throw new RangeError("min_values cannot exceed max_values.");
759
772
  }
773
+
774
+ /** Discord's flag for a message laid out with Components V2 (containers, text displays...). */
775
+ const IS_COMPONENTS_V2 = 1 << 15;
776
+
777
+ /**
778
+ * A message payload laid out with Components V2: the components, and the flag
779
+ * Discord requires for them. A V2 message cannot also carry `content` or `embeds`.
780
+ * @param components Top-level components (containers, sections, text displays, action rows...).
781
+ * @param options Extra message fields, such as `allowed_mentions` or `ephemeral`; `flags` are combined.
782
+ * @example channel.send(componentsV2Message(new ContainerBuilder().addComponents(new TextDisplayBuilder().setContent("Hi"))))
783
+ */
784
+ export function componentsV2Message<
785
+ T extends Record<string, unknown> = Record<string, never>,
786
+ >(
787
+ components: { toJSON(): APIComponent } | Array<{ toJSON(): APIComponent }>,
788
+ options?: T & { flags?: number },
789
+ ): { components: APIComponent[]; flags: number } & T {
790
+ const list = Array.isArray(components) ? components : [components];
791
+ if (!list.length)
792
+ throw new TypeError(
793
+ "A Components V2 message needs at least one component.",
794
+ );
795
+ return {
796
+ ...(options ?? ({} as T)),
797
+ components: list.map((component) => component.toJSON()),
798
+ flags: (options?.flags ?? 0) | IS_COMPONENTS_V2,
799
+ };
800
+ }
package/src/embed.ts CHANGED
@@ -32,22 +32,26 @@ export interface APIEmbed {
32
32
  /** Builds compile-time-safe rich embed payloads for Discord API requests. */
33
33
  export class EmbedBuilder {
34
34
  readonly #data: APIEmbed = {};
35
- /** Sets the embed title. @param value Title text. @returns This builder. @throws {RangeError} If title exceeds Discord's limit. */ public setTitle(
36
- value: string,
35
+ /** Sets the embed title. @param value Title text, or null to clear. @returns This builder. @throws {RangeError} If title exceeds Discord's limit. */ public setTitle(
36
+ value: string | null,
37
37
  ): this {
38
- this.#data.title = validate(value, 256, "Embed title");
38
+ if (value === null) delete this.#data.title;
39
+ else this.#data.title = validate(value, 256, "Embed title");
39
40
  return this;
40
41
  }
41
- /** Sets the embed description. @param value Description text. @returns This builder. @throws {RangeError} If description exceeds Discord's limit. */ public setDescription(
42
- value: string,
42
+ /** Sets the embed description. @param value Description text, or null to clear. @returns This builder. @throws {RangeError} If description exceeds Discord's limit. */ public setDescription(
43
+ value: string | null,
43
44
  ): this {
44
- this.#data.description = validate(value, 4096, "Embed description");
45
+ if (value === null) delete this.#data.description;
46
+ else
47
+ this.#data.description = validate(value, 4096, "Embed description");
45
48
  return this;
46
49
  }
47
- /** Sets the embed URL. @param value Absolute URL. @returns This builder. @throws {TypeError} If URL is invalid. */ public setURL(
48
- value: string,
50
+ /** Sets the embed URL. @param value Absolute URL, or null to clear. @returns This builder. @throws {TypeError} If URL is invalid. */ public setURL(
51
+ value: string | null,
49
52
  ): this {
50
- this.#data.url = url(value, "Embed URL");
53
+ if (value === null) delete this.#data.url;
54
+ else this.#data.url = url(value, "Embed URL");
51
55
  return this;
52
56
  }
53
57
  /** Sets the embed color. @param value RGB integer. @returns This builder. @throws {RangeError} If color is outside 24-bit RGB range. */ public setColor(
@@ -92,16 +96,24 @@ export class EmbedBuilder {
92
96
  };
93
97
  return this;
94
98
  }
95
- /** Sets the thumbnail URL. @param value Thumbnail URL or payload. @returns This builder. @throws {TypeError} If URL is invalid. */ public setThumbnail(
96
- value: { url: string } | string,
99
+ /** Sets the thumbnail URL. @param value Thumbnail URL or payload, or null to clear. @returns This builder. @throws {TypeError} If URL is invalid. */ public setThumbnail(
100
+ value: { url: string } | string | null,
97
101
  ): this {
102
+ if (value === null) {
103
+ delete this.#data.thumbnail;
104
+ return this;
105
+ }
98
106
  const valueURL = typeof value === "string" ? value : value.url;
99
107
  this.#data.thumbnail = { url: url(valueURL, "Thumbnail URL") };
100
108
  return this;
101
109
  }
102
- /** Sets the image URL. @param value Image URL or payload. @returns This builder. @throws {TypeError} If URL is invalid. */ public setImage(
103
- value: { url: string } | string,
110
+ /** Sets the image URL. @param value Image URL or payload, or null to clear. @returns This builder. @throws {TypeError} If URL is invalid. */ public setImage(
111
+ value: { url: string } | string | null,
104
112
  ): this {
113
+ if (value === null) {
114
+ delete this.#data.image;
115
+ return this;
116
+ }
105
117
  const valueURL = typeof value === "string" ? value : value.url;
106
118
  this.#data.image = { url: url(valueURL, "Image URL") };
107
119
  return this;
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ export {
23
23
  SeparatorBuilder,
24
24
  ThumbnailBuilder,
25
25
  ContentInventoryEntryBuilder,
26
+ componentsV2Message,
26
27
  } from "./components.js";
27
28
  export {
28
29
  SlashCommandBuilder,