district.js 0.5.0 → 0.6.0

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
@@ -110,6 +110,34 @@ client.on("interactionCreate", (i) => {
110
110
  });
111
111
  ```
112
112
 
113
+ ## Select menus & modals
114
+
115
+ ```js
116
+ import { ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder, ModalBuilder, TextInputBuilder } from "district.js";
117
+
118
+ // Dropdown
119
+ const menu = new ActionRowBuilder().addComponents(
120
+ new StringSelectMenuBuilder().setCustomId("role").setPlaceholder("Pick a role")
121
+ .addOptions({ label: "Dev", value: "dev" }, { label: "Design", value: "design" }),
122
+ );
123
+ await channel.send({ content: "Choose:", components: [menu] });
124
+
125
+ // Modal (opens instantly on click; only the submit is delivered to the bot)
126
+ const modal = new ModalBuilder().setCustomId("feedback").setTitle("Feedback")
127
+ .addComponents(new TextInputBuilder().setCustomId("msg").setLabel("Your message").setStyle("paragraph"));
128
+ const row = new ActionRowBuilder().addComponents(
129
+ new ButtonBuilder().setLabel("Give feedback").setStyle("primary").setModal(modal),
130
+ );
131
+ await channel.send({ components: [row] });
132
+
133
+ client.on("interactionCreate", (i) => {
134
+ if (i.isSelectMenu() && i.customId === "role") i.reply(`You picked ${i.values[0]}`);
135
+ if (i.isModalSubmit() && i.customId === "feedback") i.reply(`Thanks: ${i.field("msg")}`);
136
+ });
137
+ ```
138
+
139
+ > Because District delivers interactions asynchronously, modals are attached to a button and shown **instantly, client-side**; only the submitted values are sent back to your bot.
140
+
113
141
  ## Slash commands
114
142
 
115
143
  Register the command in the portal (name + handler URL), then:
@@ -1,4 +1,17 @@
1
1
  export type ButtonStyle = "primary" | "secondary" | "success" | "danger" | "link";
2
+ export interface TextInputData {
3
+ custom_id: string;
4
+ label: string;
5
+ style: "short" | "paragraph";
6
+ placeholder?: string;
7
+ required?: boolean;
8
+ max_length?: number;
9
+ }
10
+ export interface ModalData {
11
+ custom_id: string;
12
+ title: string;
13
+ fields: TextInputData[];
14
+ }
2
15
  export interface ButtonData {
3
16
  type: "button";
4
17
  style: ButtonStyle;
@@ -7,10 +20,27 @@ export interface ButtonData {
7
20
  url?: string;
8
21
  emoji?: string;
9
22
  disabled?: boolean;
23
+ /** A modal shown (instantly, client-side) when the button is clicked. */
24
+ modal?: ModalData;
25
+ }
26
+ export interface SelectOption {
27
+ label: string;
28
+ value: string;
29
+ description?: string;
30
+ }
31
+ export interface SelectData {
32
+ type: "select";
33
+ custom_id: string;
34
+ options: SelectOption[];
35
+ placeholder?: string;
36
+ min_values?: number;
37
+ max_values?: number;
38
+ disabled?: boolean;
10
39
  }
40
+ export type ComponentData = ButtonData | SelectData;
11
41
  export interface ActionRowData {
12
42
  type: "action_row";
13
- components: ButtonData[];
43
+ components: ComponentData[];
14
44
  }
15
45
  export declare class ButtonBuilder {
16
46
  data: Partial<ButtonData>;
@@ -22,12 +52,43 @@ export declare class ButtonBuilder {
22
52
  setUrl(url: string): this;
23
53
  setEmoji(emoji: string): this;
24
54
  setDisabled(disabled?: boolean): this;
55
+ /** Attach a modal — clicking the button opens it instantly; submit fires a
56
+ * modal_submit interaction with the field values. */
57
+ setModal(modal: ModalBuilder | ModalData): this;
25
58
  toJSON(): ButtonData;
26
59
  }
60
+ export declare class StringSelectMenuBuilder {
61
+ data: SelectData;
62
+ setCustomId(id: string): this;
63
+ setPlaceholder(text: string): this;
64
+ setMinValues(n: number): this;
65
+ setMaxValues(n: number): this;
66
+ setDisabled(disabled?: boolean): this;
67
+ addOptions(...options: SelectOption[]): this;
68
+ toJSON(): SelectData;
69
+ }
70
+ export declare class TextInputBuilder {
71
+ data: Partial<TextInputData>;
72
+ setCustomId(id: string): this;
73
+ setLabel(label: string): this;
74
+ setStyle(style: "short" | "paragraph"): this;
75
+ setPlaceholder(text: string): this;
76
+ setRequired(required?: boolean): this;
77
+ setMaxLength(n: number): this;
78
+ toJSON(): TextInputData;
79
+ }
80
+ export declare class ModalBuilder {
81
+ data: ModalData;
82
+ setCustomId(id: string): this;
83
+ setTitle(title: string): this;
84
+ /** Add text inputs to the modal (up to 5). */
85
+ addComponents(...inputs: (TextInputBuilder | TextInputData)[]): this;
86
+ toJSON(): ModalData;
87
+ }
27
88
  export declare class ActionRowBuilder {
28
89
  data: ActionRowData;
29
- /** Add up to 5 buttons to this row. */
30
- addComponents(...buttons: (ButtonBuilder | ButtonData)[]): this;
90
+ /** Add up to 5 buttons, or a single select menu. */
91
+ addComponents(...components: (ButtonBuilder | StringSelectMenuBuilder | ComponentData)[]): this;
31
92
  toJSON(): ActionRowData;
32
93
  }
33
94
  export type ComponentResolvable = ActionRowBuilder | ActionRowData;
@@ -12,14 +12,58 @@ export class ButtonBuilder {
12
12
  setUrl(url) { this.data.url = url; this.data.style = "link"; return this; }
13
13
  setEmoji(emoji) { this.data.emoji = emoji; return this; }
14
14
  setDisabled(disabled = true) { this.data.disabled = disabled; return this; }
15
+ /** Attach a modal — clicking the button opens it instantly; submit fires a
16
+ * modal_submit interaction with the field values. */
17
+ setModal(modal) {
18
+ this.data.modal = modal instanceof ModalBuilder ? modal.toJSON() : modal;
19
+ if (!this.data.custom_id)
20
+ this.data.custom_id = this.data.modal.custom_id;
21
+ return this;
22
+ }
23
+ toJSON() { return this.data; }
24
+ }
25
+ export class StringSelectMenuBuilder {
26
+ data = { type: "select", custom_id: "", options: [] };
27
+ setCustomId(id) { this.data.custom_id = id; return this; }
28
+ setPlaceholder(text) { this.data.placeholder = text; return this; }
29
+ setMinValues(n) { this.data.min_values = n; return this; }
30
+ setMaxValues(n) { this.data.max_values = n; return this; }
31
+ setDisabled(disabled = true) { this.data.disabled = disabled; return this; }
32
+ addOptions(...options) {
33
+ this.data.options.push(...options);
34
+ return this;
35
+ }
36
+ toJSON() { return this.data; }
37
+ }
38
+ export class TextInputBuilder {
39
+ data = { style: "short", required: true };
40
+ setCustomId(id) { this.data.custom_id = id; return this; }
41
+ setLabel(label) { this.data.label = label; return this; }
42
+ setStyle(style) { this.data.style = style; return this; }
43
+ setPlaceholder(text) { this.data.placeholder = text; return this; }
44
+ setRequired(required = true) { this.data.required = required; return this; }
45
+ setMaxLength(n) { this.data.max_length = n; return this; }
46
+ toJSON() { return this.data; }
47
+ }
48
+ export class ModalBuilder {
49
+ data = { custom_id: "", title: "", fields: [] };
50
+ setCustomId(id) { this.data.custom_id = id; return this; }
51
+ setTitle(title) { this.data.title = title; return this; }
52
+ /** Add text inputs to the modal (up to 5). */
53
+ addComponents(...inputs) {
54
+ for (const i of inputs)
55
+ this.data.fields.push(i instanceof TextInputBuilder ? i.toJSON() : i);
56
+ return this;
57
+ }
15
58
  toJSON() { return this.data; }
16
59
  }
17
60
  export class ActionRowBuilder {
18
61
  data = { type: "action_row", components: [] };
19
- /** Add up to 5 buttons to this row. */
20
- addComponents(...buttons) {
21
- for (const b of buttons)
22
- this.data.components.push(b instanceof ButtonBuilder ? b.toJSON() : b);
62
+ /** Add up to 5 buttons, or a single select menu. */
63
+ addComponents(...components) {
64
+ for (const c of components) {
65
+ this.data.components.push(c instanceof ButtonBuilder || c instanceof StringSelectMenuBuilder ? c.toJSON() : c);
66
+ }
23
67
  return this;
24
68
  }
25
69
  toJSON() { return this.data; }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { Client, type ClientEvents } from "./client.js";
2
2
  export { REST, DistrictAPIError, DEFAULT_API_BASE } from "./rest.js";
3
3
  export { ClientUser, Message, TextChannel, Interaction, User, Space, SpaceMember, Role, Attachment, Ban, Invite, Webhook, type ClientContext, type MessageContent, } from "./structures.js";
4
4
  export { EmbedBuilder, resolveEmbed, type EmbedData, type EmbedField, type EmbedResolvable } from "./embed.js";
5
- export { ButtonBuilder, ActionRowBuilder, resolveComponent, type ButtonStyle, type ButtonData, type ActionRowData, type ComponentResolvable, } from "./components.js";
5
+ export { ButtonBuilder, ActionRowBuilder, StringSelectMenuBuilder, ModalBuilder, TextInputBuilder, resolveComponent, type ButtonStyle, type ButtonData, type SelectData, type SelectOption, type ComponentData, type ModalData, type TextInputData, type ActionRowData, type ComponentResolvable, } from "./components.js";
6
6
  export { Collection } from "./collection.js";
7
7
  export { PermissionsBitField, type PermissionResolvable, type PermissionFlagName } from "./permissions.js";
8
8
  export { Formatters } from "./formatters.js";
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ export { Client } from "./client.js";
8
8
  export { REST, DistrictAPIError, DEFAULT_API_BASE } from "./rest.js";
9
9
  export { ClientUser, Message, TextChannel, Interaction, User, Space, SpaceMember, Role, Attachment, Ban, Invite, Webhook, } from "./structures.js";
10
10
  export { EmbedBuilder, resolveEmbed } from "./embed.js";
11
- export { ButtonBuilder, ActionRowBuilder, resolveComponent, } from "./components.js";
11
+ export { ButtonBuilder, ActionRowBuilder, StringSelectMenuBuilder, ModalBuilder, TextInputBuilder, resolveComponent, } from "./components.js";
12
12
  export { Collection } from "./collection.js";
13
13
  export { PermissionsBitField } from "./permissions.js";
14
14
  export { Formatters } from "./formatters.js";
@@ -205,14 +205,18 @@ export declare class Role {
205
205
  }
206
206
  export declare class Interaction {
207
207
  private client;
208
- /** "command" (slash command) or "component" (button click). */
209
- type: "command" | "component";
208
+ /** "command" (slash), "component" (button/select), or "modal_submit". */
209
+ type: "command" | "component" | "modal_submit";
210
210
  commandName: string;
211
211
  commandId: string;
212
- /** The clicked button's customId (component interactions). */
212
+ /** The component/modal customId that triggered this. */
213
213
  customId: string;
214
- /** The message the component is on (component interactions). */
214
+ /** The message the component is on. */
215
215
  messageId: string | null;
216
+ /** Selected values (select-menu interactions). */
217
+ values: string[];
218
+ /** Submitted field values keyed by input customId (modal submits). */
219
+ fields: Record<string, string>;
216
220
  spaceId: string;
217
221
  channelId: string;
218
222
  userId: string;
@@ -227,11 +231,21 @@ export declare class Interaction {
227
231
  channel_id: string;
228
232
  user_id: string;
229
233
  args?: Record<string, unknown>;
234
+ data?: {
235
+ values?: string[];
236
+ fields?: Record<string, string>;
237
+ };
230
238
  });
231
- /** True if this is a button click. */
232
- isButton(): boolean;
233
239
  /** True if this is a slash command. */
234
240
  isCommand(): boolean;
241
+ /** True if this is a button click (component with no selected values). */
242
+ isButton(): boolean;
243
+ /** True if this is a select-menu choice. */
244
+ isSelectMenu(): boolean;
245
+ /** True if this is a submitted modal. */
246
+ isModalSubmit(): boolean;
247
+ /** Get a submitted modal field value by its input customId. */
248
+ field(customId: string): string | undefined;
235
249
  /** Respond to the command by posting a message in its channel. */
236
250
  reply(content: MessageContent): Promise<Message>;
237
251
  }
@@ -376,34 +376,46 @@ export class Role {
376
376
  }
377
377
  export class Interaction {
378
378
  client;
379
- /** "command" (slash command) or "component" (button click). */
379
+ /** "command" (slash), "component" (button/select), or "modal_submit". */
380
380
  type;
381
381
  commandName;
382
382
  commandId;
383
- /** The clicked button's customId (component interactions). */
383
+ /** The component/modal customId that triggered this. */
384
384
  customId;
385
- /** The message the component is on (component interactions). */
385
+ /** The message the component is on. */
386
386
  messageId;
387
+ /** Selected values (select-menu interactions). */
388
+ values;
389
+ /** Submitted field values keyed by input customId (modal submits). */
390
+ fields;
387
391
  spaceId;
388
392
  channelId;
389
393
  userId;
390
394
  args;
391
395
  constructor(client, raw) {
392
396
  this.client = client;
393
- this.type = raw.type === "component" ? "component" : "command";
397
+ this.type = raw.type === "component" ? "component" : raw.type === "modal_submit" ? "modal_submit" : "command";
394
398
  this.commandName = raw.command ?? "";
395
399
  this.commandId = raw.command_id ?? "";
396
400
  this.customId = raw.custom_id ?? "";
397
401
  this.messageId = raw.message_id ?? null;
402
+ this.values = Array.isArray(raw.data?.values) ? raw.data.values : [];
403
+ this.fields = raw.data?.fields && typeof raw.data.fields === "object" ? raw.data.fields : {};
398
404
  this.spaceId = raw.space_id;
399
405
  this.channelId = raw.channel_id;
400
406
  this.userId = raw.user_id;
401
407
  this.args = raw.args ?? {};
402
408
  }
403
- /** True if this is a button click. */
404
- isButton() { return this.type === "component"; }
405
409
  /** True if this is a slash command. */
406
410
  isCommand() { return this.type === "command"; }
411
+ /** True if this is a button click (component with no selected values). */
412
+ isButton() { return this.type === "component" && this.values.length === 0; }
413
+ /** True if this is a select-menu choice. */
414
+ isSelectMenu() { return this.type === "component" && this.values.length > 0; }
415
+ /** True if this is a submitted modal. */
416
+ isModalSubmit() { return this.type === "modal_submit"; }
417
+ /** Get a submitted modal field value by its input customId. */
418
+ field(customId) { return this.fields[customId]; }
407
419
  /** Respond to the command by posting a message in its channel. */
408
420
  async reply(content) {
409
421
  const p = toPayload(content);
package/dist/types.d.ts CHANGED
@@ -146,7 +146,7 @@ export type DistrictEvent = {
146
146
  emoji: string;
147
147
  } | {
148
148
  event: "interaction.create";
149
- type?: "command" | "component";
149
+ type?: "command" | "component" | "modal_submit";
150
150
  command?: string;
151
151
  command_id?: string;
152
152
  custom_id?: string;
@@ -155,6 +155,10 @@ export type DistrictEvent = {
155
155
  channel_id: string;
156
156
  user_id: string;
157
157
  args?: Record<string, unknown>;
158
+ data?: {
159
+ values?: string[];
160
+ fields?: Record<string, string>;
161
+ };
158
162
  };
159
163
  export interface ReactionEvent {
160
164
  spaceId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "district.js",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "A powerful Node.js module for building District bots — post, react, run slash commands, and receive events.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",