chrxmaticc-framework 1.2.1 → 1.4.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.
@@ -0,0 +1,652 @@
1
+ /**
2
+ * core/ChrxComponentsV3.js
3
+ * Components V3 — every Discord component simplified into clean Chrx builders.
4
+ *
5
+ * Includes:
6
+ * - ChrxButton
7
+ * - ChrxSelectMenu
8
+ * - ChrxPagination
9
+ * - ChrxContextMenu
10
+ * - ChrxMenu
11
+ * - ChrxForm
12
+ * - ChrxConfirm
13
+ */
14
+
15
+ const {
16
+ ButtonBuilder,
17
+ ButtonStyle,
18
+ ActionRowBuilder,
19
+ StringSelectMenuBuilder,
20
+ StringSelectMenuOptionBuilder,
21
+ ContextMenuCommandBuilder,
22
+ ApplicationCommandType,
23
+ EmbedBuilder,
24
+ ComponentType,
25
+ } = require("discord.js");
26
+
27
+ // ── Style map ─────────────────────────────────────────────────────────────
28
+ const BUTTON_STYLE_MAP = {
29
+ primary: ButtonStyle.Primary,
30
+ secondary: ButtonStyle.Secondary,
31
+ success: ButtonStyle.Success,
32
+ danger: ButtonStyle.Danger,
33
+ link: ButtonStyle.Link,
34
+ blurple: ButtonStyle.Primary,
35
+ grey: ButtonStyle.Secondary,
36
+ gray: ButtonStyle.Secondary,
37
+ green: ButtonStyle.Success,
38
+ red: ButtonStyle.Danger,
39
+ };
40
+
41
+ // ═════════════════════════════════════════════════════════════════════════════
42
+ // ChrxButton
43
+ // ═════════════════════════════════════════════════════════════════════════════
44
+
45
+ class ChrxButton {
46
+ /**
47
+ * @param {object} options
48
+ * @param {string} options.label Button label
49
+ * @param {string} [options.style] "primary"|"secondary"|"success"|"danger"|"link" (default: primary)
50
+ * @param {string} [options.id] Custom ID (not needed for link buttons)
51
+ * @param {string} [options.url] URL for link buttons
52
+ * @param {string} [options.emoji] Emoji for button
53
+ * @param {boolean} [options.disabled] Disable the button
54
+ * @param {Function} [options.onClick] Handler (interaction) => void
55
+ */
56
+ constructor(options = {}) {
57
+ if (!options.label) throw new Error("[ChrxButton] label is required.");
58
+
59
+ this._options = options;
60
+ this._handler = options.onClick ?? null;
61
+
62
+ const btn = new ButtonBuilder().setLabel(options.label);
63
+
64
+ if (options.style === "link" || options.url) {
65
+ btn.setStyle(ButtonStyle.Link).setURL(options.url);
66
+ } else {
67
+ btn.setStyle(BUTTON_STYLE_MAP[options.style ?? "primary"] ?? ButtonStyle.Primary);
68
+ btn.setCustomId(options.id ?? `chrx-btn-${Math.random().toString(36).slice(2, 9)}`);
69
+ }
70
+
71
+ if (options.emoji) btn.setEmoji(options.emoji);
72
+ if (options.disabled) btn.setDisabled(true);
73
+
74
+ this._button = btn;
75
+ this.customId = options.id;
76
+ }
77
+
78
+ /**
79
+ * Build into an ActionRow ready to send.
80
+ */
81
+ toRow() {
82
+ return new ActionRowBuilder().addComponents(this._button);
83
+ }
84
+
85
+ /**
86
+ * Send the button with a message and listen for clicks.
87
+ * Auto cleans up the collector after timeout.
88
+ *
89
+ * @param {import("discord.js").CommandInteraction} interaction
90
+ * @param {object} [options]
91
+ * @param {string} [options.content] Message content
92
+ * @param {object} [options.embed] EmbedBuilder to attach
93
+ * @param {boolean} [options.ephemeral]
94
+ * @param {number} [options.timeout] Collector timeout in ms (default: 60000)
95
+ */
96
+ async send(interaction, options = {}) {
97
+ const payload = {
98
+ components: [this.toRow()],
99
+ ephemeral: options.ephemeral ?? false,
100
+ };
101
+ if (options.content) payload.content = options.content;
102
+ if (options.embed) payload.embeds = [options.embed];
103
+
104
+ const reply = await interaction.reply({ ...payload, fetchReply: true });
105
+
106
+ if (!this._handler) return;
107
+
108
+ const collector = reply.createMessageComponentCollector({
109
+ componentType: ComponentType.Button,
110
+ time: options.timeout ?? 60000,
111
+ });
112
+
113
+ collector.on("collect", async (i) => {
114
+ if (i.customId !== (this._options.id ?? this._button.data.custom_id)) return;
115
+ try {
116
+ await this._handler(i);
117
+ } catch (err) {
118
+ console.error("[ChrxButton] onClick error:", err);
119
+ i.reply({ content: err.message || "❌ Something went wrong.", ephemeral: true }).catch(() => {});
120
+ }
121
+ });
122
+
123
+ collector.on("end", () => {
124
+ reply.edit({ components: [] }).catch(() => {});
125
+ });
126
+ }
127
+ }
128
+
129
+ // ═════════════════════════════════════════════════════════════════════════════
130
+ // ChrxSelectMenu
131
+ // ═════════════════════════════════════════════════════════════════════════════
132
+
133
+ class ChrxSelectMenu {
134
+ /**
135
+ * @param {object} options
136
+ * @param {string} options.id Custom ID
137
+ * @param {string} options.placeholder Placeholder text
138
+ * @param {object[]} options.options Menu options
139
+ * @param {string} options.options[].label
140
+ * @param {string} options.options[].value
141
+ * @param {string} [options.options[].description]
142
+ * @param {string} [options.options[].emoji]
143
+ * @param {boolean} [options.options[].default]
144
+ * @param {number} [options.min] Min selections (default: 1)
145
+ * @param {number} [options.max] Max selections (default: 1)
146
+ * @param {Function} [options.onSelect] Handler (interaction, values) => void
147
+ */
148
+ constructor(options = {}) {
149
+ if (!options.id) throw new Error("[ChrxSelectMenu] id is required.");
150
+ if (!options.placeholder) throw new Error("[ChrxSelectMenu] placeholder is required.");
151
+ if (!options.options?.length) throw new Error("[ChrxSelectMenu] options are required.");
152
+
153
+ this._options = options;
154
+ this._handler = options.onSelect ?? null;
155
+
156
+ const menu = new StringSelectMenuBuilder()
157
+ .setCustomId(options.id)
158
+ .setPlaceholder(options.placeholder)
159
+ .setMinValues(options.min ?? 1)
160
+ .setMaxValues(options.max ?? 1)
161
+ .addOptions(
162
+ options.options.map((opt) => {
163
+ const o = new StringSelectMenuOptionBuilder()
164
+ .setLabel(opt.label)
165
+ .setValue(opt.value);
166
+ if (opt.description) o.setDescription(opt.description);
167
+ if (opt.emoji) o.setEmoji(opt.emoji);
168
+ if (opt.default) o.setDefault(true);
169
+ return o;
170
+ })
171
+ );
172
+
173
+ this._menu = menu;
174
+ }
175
+
176
+ toRow() {
177
+ return new ActionRowBuilder().addComponents(this._menu);
178
+ }
179
+
180
+ /**
181
+ * Send the select menu and listen for selections.
182
+ */
183
+ async send(interaction, options = {}) {
184
+ const payload = {
185
+ components: [this.toRow()],
186
+ ephemeral: options.ephemeral ?? false,
187
+ };
188
+ if (options.content) payload.content = options.content;
189
+ if (options.embed) payload.embeds = [options.embed];
190
+
191
+ const reply = await interaction.reply({ ...payload, fetchReply: true });
192
+
193
+ if (!this._handler) return;
194
+
195
+ const collector = reply.createMessageComponentCollector({
196
+ componentType: ComponentType.StringSelect,
197
+ time: options.timeout ?? 60000,
198
+ });
199
+
200
+ collector.on("collect", async (i) => {
201
+ if (i.customId !== this._options.id) return;
202
+ try {
203
+ await this._handler(i, i.values);
204
+ } catch (err) {
205
+ console.error("[ChrxSelectMenu] onSelect error:", err);
206
+ i.reply({ content: err.message || "❌ Something went wrong.", ephemeral: true }).catch(() => {});
207
+ }
208
+ });
209
+
210
+ collector.on("end", () => {
211
+ reply.edit({ components: [] }).catch(() => {});
212
+ });
213
+ }
214
+ }
215
+
216
+ // ═════════════════════════════════════════════════════════════════════════════
217
+ // ChrxPagination
218
+ // ═════════════════════════════════════════════════════════════════════════════
219
+
220
+ class ChrxPagination {
221
+ /**
222
+ * Auto paginated embed system with next/prev buttons.
223
+ *
224
+ * @param {object} options
225
+ * @param {EmbedBuilder[]} options.pages Array of EmbedBuilders
226
+ * @param {number} [options.timeout] Collector timeout ms (default: 120000)
227
+ * @param {boolean} [options.ephemeral]
228
+ * @param {boolean} [options.showPageNumber] Show "Page X/Y" in footer (default: true)
229
+ */
230
+ constructor(options = {}) {
231
+ if (!options.pages?.length) throw new Error("[ChrxPagination] pages array is required.");
232
+ this._options = options;
233
+ this._page = 0;
234
+ }
235
+
236
+ _getButtons(page, total) {
237
+ const prev = new ButtonBuilder()
238
+ .setCustomId("chrx-page-prev")
239
+ .setLabel("◀")
240
+ .setStyle(ButtonStyle.Secondary)
241
+ .setDisabled(page === 0);
242
+
243
+ const next = new ButtonBuilder()
244
+ .setCustomId("chrx-page-next")
245
+ .setLabel("▶")
246
+ .setStyle(ButtonStyle.Secondary)
247
+ .setDisabled(page === total - 1);
248
+
249
+ const counter = new ButtonBuilder()
250
+ .setCustomId("chrx-page-counter")
251
+ .setLabel(`${page + 1} / ${total}`)
252
+ .setStyle(ButtonStyle.Primary)
253
+ .setDisabled(true);
254
+
255
+ return new ActionRowBuilder().addComponents(prev, counter, next);
256
+ }
257
+
258
+ _getPage(index) {
259
+ const page = this._options.pages[index];
260
+ const total = this._options.pages.length;
261
+ if (this._options.showPageNumber !== false) {
262
+ page.setFooter({ text: `Page ${index + 1} of ${total}` });
263
+ }
264
+ return page;
265
+ }
266
+
267
+ /**
268
+ * Send the paginated embed.
269
+ */
270
+ async send(interaction) {
271
+ const total = this._options.pages.length;
272
+ const reply = await interaction.reply({
273
+ embeds: [this._getPage(0)],
274
+ components: total > 1 ? [this._getButtons(0, total)] : [],
275
+ ephemeral: this._options.ephemeral ?? false,
276
+ fetchReply: true,
277
+ });
278
+
279
+ if (total <= 1) return;
280
+
281
+ const collector = reply.createMessageComponentCollector({
282
+ componentType: ComponentType.Button,
283
+ time: this._options.timeout ?? 120000,
284
+ });
285
+
286
+ collector.on("collect", async (i) => {
287
+ if (i.user.id !== interaction.user.id) {
288
+ return i.reply({ content: "❌ These controls aren't for you.", ephemeral: true });
289
+ }
290
+
291
+ if (i.customId === "chrx-page-prev") this._page = Math.max(0, this._page - 1);
292
+ if (i.customId === "chrx-page-next") this._page = Math.min(total - 1, this._page + 1);
293
+
294
+ await i.update({
295
+ embeds: [this._getPage(this._page)],
296
+ components: [this._getButtons(this._page, total)],
297
+ });
298
+ });
299
+
300
+ collector.on("end", () => {
301
+ reply.edit({ components: [] }).catch(() => {});
302
+ });
303
+ }
304
+ }
305
+
306
+ // ═════════════════════════════════════════════════════════════════════════════
307
+ // ChrxContextMenu
308
+ // ═════════════════════════════════════════════════════════════════════════════
309
+
310
+ class ChrxContextMenu {
311
+ /**
312
+ * Right click context menu command.
313
+ *
314
+ * @param {object} options
315
+ * @param {string} options.name Context menu name
316
+ * @param {string} options.type "user" | "message"
317
+ * @param {Function} options.run Handler (interaction) => void
318
+ */
319
+ constructor(options = {}) {
320
+ if (!options.name) throw new Error("[ChrxContextMenu] name is required.");
321
+ if (!options.type) throw new Error("[ChrxContextMenu] type is required. Use 'user' or 'message'.");
322
+ if (!options.run) throw new Error("[ChrxContextMenu] run function is required.");
323
+
324
+ const builder = new ContextMenuCommandBuilder()
325
+ .setName(options.name)
326
+ .setType(
327
+ options.type === "user"
328
+ ? ApplicationCommandType.User
329
+ : ApplicationCommandType.Message
330
+ );
331
+
332
+ this.data = builder;
333
+ this.execute = async (interaction) => {
334
+ try {
335
+ await options.run(interaction);
336
+ } catch (err) {
337
+ console.error(`[ChrxContextMenu] Error in "${options.name}":`, err);
338
+ const msg = { content: err.message || "❌ Something went wrong.", ephemeral: true };
339
+ if (interaction.replied || interaction.deferred) {
340
+ interaction.followUp(msg).catch(() => {});
341
+ } else {
342
+ interaction.reply(msg).catch(() => {});
343
+ }
344
+ }
345
+ };
346
+ }
347
+ }
348
+
349
+ // ═════════════════════════════════════════════════════════════════════════════
350
+ // ChrxMenu
351
+ // ═════════════════════════════════════════════════════════════════════════════
352
+
353
+ class ChrxMenu {
354
+ /**
355
+ * Full interactive menu — combines embed, buttons and select menus together.
356
+ *
357
+ * @param {object} options
358
+ * @param {object} options.embed Embed options for ChrxEmbedBuilder.build()
359
+ * @param {object[]} [options.buttons] Array of ChrxButton-like option objects
360
+ * @param {object} [options.select] ChrxSelectMenu-like option object
361
+ * @param {number} [options.timeout] Collector timeout ms (default: 60000)
362
+ * @param {boolean} [options.ephemeral]
363
+ */
364
+ constructor(options = {}) {
365
+ if (!options.embed) throw new Error("[ChrxMenu] embed is required.");
366
+ this._options = options;
367
+ }
368
+
369
+ async send(interaction) {
370
+ const { EmbedBuilder } = require("discord.js");
371
+ const embed = new EmbedBuilder();
372
+
373
+ if (this._options.embed.title) embed.setTitle(this._options.embed.title);
374
+ if (this._options.embed.desc) embed.setDescription(this._options.embed.desc);
375
+ if (this._options.embed.color) embed.setColor(this._options.embed.color);
376
+ if (this._options.embed.footer) embed.setFooter({ text: this._options.embed.footer });
377
+ if (this._options.embed.thumbnail) embed.setThumbnail(this._options.embed.thumbnail);
378
+ if (this._options.embed.timestamp) embed.setTimestamp();
379
+
380
+ const rows = [];
381
+
382
+ // ── Buttons ───────────────────────────────────────────────────────────
383
+ if (this._options.buttons?.length) {
384
+ const btnRow = new ActionRowBuilder();
385
+ const handlers = {};
386
+
387
+ for (const btnOpt of this._options.buttons) {
388
+ const btn = new ButtonBuilder()
389
+ .setLabel(btnOpt.label)
390
+ .setStyle(BUTTON_STYLE_MAP[btnOpt.style ?? "primary"] ?? ButtonStyle.Primary)
391
+ .setCustomId(btnOpt.id ?? `chrx-menu-btn-${Math.random().toString(36).slice(2, 7)}`);
392
+
393
+ if (btnOpt.emoji) btn.setEmoji(btnOpt.emoji);
394
+ if (btnOpt.disabled) btn.setDisabled(true);
395
+
396
+ handlers[btnOpt.id] = btnOpt.onClick;
397
+ btnRow.addComponents(btn);
398
+ }
399
+
400
+ rows.push(btnRow);
401
+
402
+ const reply = await interaction.reply({
403
+ embeds: [embed],
404
+ components: rows,
405
+ ephemeral: this._options.ephemeral ?? false,
406
+ fetchReply: true,
407
+ });
408
+
409
+ const collector = reply.createMessageComponentCollector({
410
+ time: this._options.timeout ?? 60000,
411
+ });
412
+
413
+ collector.on("collect", async (i) => {
414
+ const handler = handlers[i.customId];
415
+ if (handler) {
416
+ try {
417
+ await handler(i);
418
+ } catch (err) {
419
+ i.reply({ content: err.message || "❌ Error.", ephemeral: true }).catch(() => {});
420
+ }
421
+ }
422
+ });
423
+
424
+ collector.on("end", () => {
425
+ reply.edit({ components: [] }).catch(() => {});
426
+ });
427
+
428
+ return;
429
+ }
430
+
431
+ // ── Select only ───────────────────────────────────────────────────────
432
+ if (this._options.select) {
433
+ const menu = new ChrxSelectMenu(this._options.select);
434
+ rows.push(menu.toRow());
435
+ await interaction.reply({
436
+ embeds: [embed],
437
+ components: rows,
438
+ ephemeral: this._options.ephemeral ?? false,
439
+ });
440
+ return;
441
+ }
442
+
443
+ // ── Embed only ────────────────────────────────────────────────────────
444
+ await interaction.reply({
445
+ embeds: [embed],
446
+ ephemeral: this._options.ephemeral ?? false,
447
+ });
448
+ }
449
+ }
450
+
451
+ // ═════════════════════════════════════════════════════════════════════════════
452
+ // ChrxForm
453
+ // ═════════════════════════════════════════════════════════════════════════════
454
+
455
+ class ChrxForm {
456
+ /**
457
+ * Combines a modal AND processes the response in one block.
458
+ * No need to set up onModalSubmit separately.
459
+ *
460
+ * @param {object} options
461
+ * @param {string} options.id Modal ID
462
+ * @param {string} options.title Modal title
463
+ * @param {object[]} options.inputs Same as ChrxModalBuilder inputs
464
+ * @param {Function} options.onSubmit (interaction, values) => void
465
+ * values is an object keyed by input id e.g. { answer: "yes" }
466
+ */
467
+ constructor(options = {}) {
468
+ if (!options.id) throw new Error("[ChrxForm] id is required.");
469
+ if (!options.title) throw new Error("[ChrxForm] title is required.");
470
+ if (!options.inputs?.length) throw new Error("[ChrxForm] inputs are required.");
471
+ if (!options.onSubmit) throw new Error("[ChrxForm] onSubmit is required.");
472
+
473
+ this._options = options;
474
+ this._modal = null;
475
+
476
+ // Build the modal
477
+ const {
478
+ ModalBuilder,
479
+ TextInputBuilder,
480
+ TextInputStyle,
481
+ ActionRowBuilder,
482
+ } = require("discord.js");
483
+
484
+ const STYLE_MAP = {
485
+ short: TextInputStyle.Short,
486
+ paragraph: TextInputStyle.Paragraph,
487
+ long: TextInputStyle.Paragraph,
488
+ };
489
+
490
+ const modal = new ModalBuilder()
491
+ .setCustomId(options.id)
492
+ .setTitle(options.title);
493
+
494
+ const rows = options.inputs.map((input) => {
495
+ const textInput = new TextInputBuilder()
496
+ .setCustomId(input.id)
497
+ .setLabel(input.label)
498
+ .setStyle(STYLE_MAP[input.type ?? "short"] ?? TextInputStyle.Short);
499
+
500
+ if (input.required) textInput.setRequired(input.required);
501
+ if (input.placeholder) textInput.setPlaceholder(input.placeholder);
502
+ if (input.value) textInput.setValue(input.value);
503
+ if (input.minLength) textInput.setMinLength(input.minLength);
504
+ if (input.maxLength) textInput.setMaxLength(input.maxLength);
505
+
506
+ return new ActionRowBuilder().addComponents(textInput);
507
+ });
508
+
509
+ modal.addComponents(...rows);
510
+ this._modal = modal;
511
+ }
512
+
513
+ /**
514
+ * Show the form and handle submission automatically.
515
+ * @param {import("discord.js").CommandInteraction} interaction
516
+ */
517
+ async show(interaction) {
518
+ await interaction.showModal(this._modal);
519
+
520
+ try {
521
+ const submitted = await interaction.awaitModalSubmit({
522
+ time: 300000, // 5 minutes to fill out the form
523
+ filter: (i) => i.customId === this._options.id && i.user.id === interaction.user.id,
524
+ });
525
+
526
+ // Build values object keyed by input id
527
+ const values = {};
528
+ for (const input of this._options.inputs) {
529
+ values[input.id] = submitted.fields.getTextInputValue(input.id);
530
+ }
531
+
532
+ try {
533
+ await this._options.onSubmit(submitted, values);
534
+ } catch (err) {
535
+ console.error(`[ChrxForm] onSubmit error:`, err);
536
+ const msg = { content: err.message || "❌ Something went wrong.", ephemeral: true };
537
+ if (submitted.replied || submitted.deferred) {
538
+ submitted.followUp(msg).catch(() => {});
539
+ } else {
540
+ submitted.reply(msg).catch(() => {});
541
+ }
542
+ }
543
+ } catch {
544
+ // User didn't submit in time — silently ignore
545
+ }
546
+ }
547
+ }
548
+
549
+ // ═════════════════════════════════════════════════════════════════════════════
550
+ // ChrxConfirm
551
+ // ═════════════════════════════════════════════════════════════════════════════
552
+
553
+ class ChrxConfirm {
554
+ /**
555
+ * Yes/No confirmation prompt in two lines.
556
+ *
557
+ * @param {object} options
558
+ * @param {string} options.question Question to ask
559
+ * @param {Function} options.onConfirm Called when user clicks Yes
560
+ * @param {Function} [options.onCancel] Called when user clicks No
561
+ * @param {string} [options.confirmLabel] Yes button label (default: "Confirm")
562
+ * @param {string} [options.cancelLabel] No button label (default: "Cancel")
563
+ * @param {string} [options.color] Embed color (default: #5865F2)
564
+ * @param {boolean} [options.ephemeral]
565
+ * @param {number} [options.timeout] ms before auto cancel (default: 30000)
566
+ */
567
+ constructor(options = {}) {
568
+ if (!options.question) throw new Error("[ChrxConfirm] question is required.");
569
+ if (!options.onConfirm) throw new Error("[ChrxConfirm] onConfirm is required.");
570
+ this._options = options;
571
+ }
572
+
573
+ /**
574
+ * Send the confirmation prompt.
575
+ */
576
+ async send(interaction) {
577
+ const confirmId = `chrx-confirm-yes-${Math.random().toString(36).slice(2, 7)}`;
578
+ const cancelId = `chrx-confirm-no-${Math.random().toString(36).slice(2, 7)}`;
579
+
580
+ const embed = new EmbedBuilder()
581
+ .setColor(this._options.color ?? "#5865F2")
582
+ .setDescription(`❓ ${this._options.question}`);
583
+
584
+ const row = new ActionRowBuilder().addComponents(
585
+ new ButtonBuilder()
586
+ .setCustomId(confirmId)
587
+ .setLabel(this._options.confirmLabel ?? "Confirm")
588
+ .setStyle(ButtonStyle.Success),
589
+ new ButtonBuilder()
590
+ .setCustomId(cancelId)
591
+ .setLabel(this._options.cancelLabel ?? "Cancel")
592
+ .setStyle(ButtonStyle.Danger)
593
+ );
594
+
595
+ const reply = await interaction.reply({
596
+ embeds: [embed],
597
+ components: [row],
598
+ ephemeral: this._options.ephemeral ?? false,
599
+ fetchReply: true,
600
+ });
601
+
602
+ const collector = reply.createMessageComponentCollector({
603
+ componentType: ComponentType.Button,
604
+ time: this._options.timeout ?? 30000,
605
+ max: 1,
606
+ });
607
+
608
+ collector.on("collect", async (i) => {
609
+ if (i.user.id !== interaction.user.id) {
610
+ return i.reply({ content: "❌ This isn't your confirmation.", ephemeral: true });
611
+ }
612
+
613
+ try {
614
+ if (i.customId === confirmId) {
615
+ await this._options.onConfirm(i);
616
+ } else {
617
+ if (this._options.onCancel) {
618
+ await this._options.onCancel(i);
619
+ } else {
620
+ await i.update({ embeds: [embed.setDescription("❌ Cancelled.").setColor("#ED4245")], components: [] });
621
+ }
622
+ }
623
+ } catch (err) {
624
+ console.error("[ChrxConfirm] Error:", err);
625
+ i.reply({ content: err.message || "❌ Something went wrong.", ephemeral: true }).catch(() => {});
626
+ }
627
+ });
628
+
629
+ collector.on("end", (collected) => {
630
+ if (collected.size === 0) {
631
+ reply.edit({
632
+ embeds: [embed.setDescription("⏱ Confirmation timed out.").setColor("#ED4245")],
633
+ components: [],
634
+ }).catch(() => {});
635
+ }
636
+ });
637
+ }
638
+ }
639
+
640
+ // ═════════════════════════════════════════════════════════════════════════════
641
+ // Exports
642
+ // ═════════════════════════════════════════════════════════════════════════════
643
+
644
+ module.exports = {
645
+ ChrxButton,
646
+ ChrxSelectMenu,
647
+ ChrxPagination,
648
+ ChrxContextMenu,
649
+ ChrxMenu,
650
+ ChrxForm,
651
+ ChrxConfirm,
652
+ };