seedcord 0.14.0 → 0.15.0-next.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/dist/index.cjs CHANGED
@@ -467,7 +467,7 @@ function AutocompleteRoute(...routes) {
467
467
  * @example
468
468
  * ```typescript
469
469
  * \@SelectMenuRoute(SelectMenuKind.User, AssignId)
470
- * class AssignSelect extends SelectHandler<SelectMenuKind.User, [typeof AssignId]> {
470
+ * class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
471
471
  * // handles user select menus minted from AssignId
472
472
  * }
473
473
  * ```
@@ -1675,9 +1675,8 @@ var ReplySender = class {
1675
1675
  async dispatch(response, ephemeral) {
1676
1676
  if (this.interaction.replied) return await this.interaction.followUp(this.replyOptions(response, ephemeral));
1677
1677
  if (this.interaction.deferred) {
1678
- const message = await this.interaction.followUp(this.replyOptions(response, ephemeral));
1679
- if (!this.interaction.isMessageComponent() && !this.interaction.isModalSubmit()) await this.clearStaleDefer();
1680
- return message;
1678
+ if (this.interaction.ephemeral === null) return await this.interaction.followUp(this.replyOptions(response, ephemeral));
1679
+ return await this.interaction.editReply(this.editBody(response));
1681
1680
  }
1682
1681
  await this.interaction.reply(this.replyOptions(response, ephemeral));
1683
1682
  return await this.interaction.fetchReply();
@@ -1698,13 +1697,6 @@ var ReplySender = class {
1698
1697
  ...response.files && { files: response.files }
1699
1698
  };
1700
1699
  }
1701
- async clearStaleDefer() {
1702
- try {
1703
- await this.interaction.deleteReply();
1704
- } catch (error) {
1705
- this.logSwallowed("clear stale defer", error);
1706
- }
1707
- }
1708
1700
  logSwallowed(action, error) {
1709
1701
  if (error instanceof discord_js.DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
1710
1702
  this.logger.debug(`reply ${action} hit harmless code ${error.code}`);
@@ -1939,7 +1931,7 @@ var InteractionMiddleware = class extends BaseHandler {
1939
1931
  * Shared base the typed interaction handlers extend.
1940
1932
  *
1941
1933
  * Not a public entry point. You should be using {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1942
- * or {@link SelectHandler} instead. This class only carries the repliable-event plumbing those bases share,
1934
+ * or {@link SelectMenuHandler} instead. This class only carries the repliable-event plumbing those bases share,
1943
1935
  * so DO NOT use it directly.
1944
1936
  *
1945
1937
  * @typeParam Repliable - The interaction type this handler processes
@@ -2160,7 +2152,7 @@ var AutocompleteHandler = class extends BaseHandler {
2160
2152
  /**
2161
2153
  * Shared base the customId-routed component handlers extend.
2162
2154
  *
2163
- * Not a public entry point. You should be using {@link ButtonHandler}, {@link SelectHandler}, or
2155
+ * Not a public entry point. You should be using {@link ButtonHandler}, {@link SelectMenuHandler}, or
2164
2156
  * {@link ModalHandler} instead. This class only carries the customId decode and route-matching plumbing
2165
2157
  * those bases share, so DO NOT use it directly.
2166
2158
  *
@@ -2276,7 +2268,7 @@ var ButtonHandler = class extends ComponentHandler {};
2276
2268
  var ModalHandler = class extends ComponentHandler {};
2277
2269
 
2278
2270
  //#endregion
2279
- //#region src/handlers/interaction/components/SelectHandler.ts
2271
+ //#region src/handlers/interaction/components/SelectMenuHandler.ts
2280
2272
  /**
2281
2273
  * Base class for a select menu handler.
2282
2274
  *
@@ -2291,7 +2283,7 @@ var ModalHandler = class extends ComponentHandler {};
2291
2283
  * @example
2292
2284
  * ```ts
2293
2285
  * \@SelectMenuRoute(SelectMenuKind.User, AssignId)
2294
- * class AssignSelect extends SelectHandler<SelectMenuKind.User, [typeof AssignId]> {
2286
+ * class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
2295
2287
  * async execute() {
2296
2288
  * const { roleId } = this.params;
2297
2289
  * await this.event.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
@@ -2299,7 +2291,7 @@ var ModalHandler = class extends ComponentHandler {};
2299
2291
  * }
2300
2292
  * ```
2301
2293
  */
2302
- var SelectHandler = class extends ComponentHandler {};
2294
+ var SelectMenuHandler = class extends ComponentHandler {};
2303
2295
 
2304
2296
  //#endregion
2305
2297
  //#region src/handlers/event/EventHandler.ts
@@ -2395,6 +2387,239 @@ var EventMiddleware = class extends BaseHandler {
2395
2387
  }
2396
2388
  };
2397
2389
 
2390
+ //#endregion
2391
+ //#region src/pagination/controls.ts
2392
+ const DEFAULT_LABEL = {
2393
+ first: "First",
2394
+ prev: "Prev",
2395
+ indicator: "",
2396
+ next: "Next",
2397
+ last: "Last"
2398
+ };
2399
+ const CONTROL_SLOT = {
2400
+ first: 0,
2401
+ prev: 1,
2402
+ indicator: 2,
2403
+ next: 3,
2404
+ last: 4
2405
+ };
2406
+ const MAX_BUTTONS_PER_ROW = 5;
2407
+ var Controls = class {
2408
+ cursor;
2409
+ view;
2410
+ constructor(cursor, view) {
2411
+ this.cursor = cursor;
2412
+ this.view = view;
2413
+ }
2414
+ button(key, cosmetics) {
2415
+ const { target, disabled } = this.navState(key);
2416
+ const defaultLabel = key === "indicator" ? this.indicatorText() : DEFAULT_LABEL[key];
2417
+ const label = cosmetics?.label ?? (key === "indicator" || !cosmetics?.emoji ? defaultLabel : void 0);
2418
+ const button = new discord_js.ButtonBuilder().setCustomId(this.cursor.encode({
2419
+ page: Math.min(Math.max(0, target), _seedcord_kit_internal.PAGE_MAX),
2420
+ slot: CONTROL_SLOT[key]
2421
+ })).setStyle(cosmetics?.style ?? discord_js.ButtonStyle.Secondary).setDisabled(disabled);
2422
+ if (label !== void 0) button.setLabel(label);
2423
+ if (cosmetics?.emoji) button.setEmoji(cosmetics.emoji);
2424
+ return button;
2425
+ }
2426
+ navState(key) {
2427
+ const { page, totalPages, hasPrev, hasNext } = this.view;
2428
+ const knownTotal = totalPages !== void 0;
2429
+ return {
2430
+ target: {
2431
+ first: 0,
2432
+ prev: page - 1,
2433
+ indicator: page,
2434
+ next: page + 1,
2435
+ last: knownTotal ? totalPages - 1 : page
2436
+ }[key],
2437
+ disabled: {
2438
+ first: !hasPrev,
2439
+ prev: !hasPrev,
2440
+ indicator: true,
2441
+ next: !hasNext || page + 1 > _seedcord_kit_internal.PAGE_MAX,
2442
+ last: !hasNext || !knownTotal || totalPages - 1 > _seedcord_kit_internal.PAGE_MAX
2443
+ }[key]
2444
+ };
2445
+ }
2446
+ row(...keys) {
2447
+ if (keys.length < 1) throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.PaginationEmptyControls);
2448
+ if (keys.length > MAX_BUTTONS_PER_ROW) throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.PaginationTooManyControls, [keys.length]);
2449
+ const seen = /* @__PURE__ */ new Set();
2450
+ for (const key of keys) {
2451
+ if (seen.has(key)) throw new _seedcord_errors_internal.SeedcordTypeError(_seedcord_errors.SeedcordErrorCode.PaginationDuplicateControls, [key]);
2452
+ seen.add(key);
2453
+ }
2454
+ return new discord_js.ActionRowBuilder().addComponents(keys.map((key) => this.button(key)));
2455
+ }
2456
+ indicatorText() {
2457
+ const { page, totalPages } = this.view;
2458
+ return totalPages === void 0 ? `Page ${page + 1}` : `Page ${page + 1} of ${totalPages}`;
2459
+ }
2460
+ };
2461
+
2462
+ //#endregion
2463
+ //#region src/pagination/render.ts
2464
+ var PageContainer = class extends _seedcord_kit.BuilderComponent {
2465
+ constructor(view, renderItem, controlRow) {
2466
+ super("container");
2467
+ const base = view.page * view.perPage;
2468
+ let lines = [];
2469
+ const flush = () => {
2470
+ if (lines.length === 0) return;
2471
+ this.instance.addTextDisplayComponents(new discord_js.TextDisplayBuilder().setContent(lines.join("\n")));
2472
+ lines = [];
2473
+ };
2474
+ view.items.forEach((item, offset) => {
2475
+ const rendered = renderItem(item, base + offset);
2476
+ if (typeof rendered === "string") {
2477
+ lines.push(rendered);
2478
+ return;
2479
+ }
2480
+ flush();
2481
+ this.instance.addSectionComponents(rendered.component);
2482
+ });
2483
+ flush();
2484
+ this.instance.addActionRowComponents(controlRow);
2485
+ }
2486
+ };
2487
+ function toReplyResponse(renderable) {
2488
+ if (typeof renderable === "string") return { components: [new discord_js.TextDisplayBuilder().setContent(renderable)] };
2489
+ if (Array.isArray(renderable)) return { components: renderable };
2490
+ return renderable;
2491
+ }
2492
+ const ARRAY_KEYS = [
2493
+ "first",
2494
+ "prev",
2495
+ "indicator",
2496
+ "next",
2497
+ "last"
2498
+ ];
2499
+ const CURSOR_KEYS = [
2500
+ "prev",
2501
+ "indicator",
2502
+ "next"
2503
+ ];
2504
+ /**
2505
+ * Build a page's V2 reply. A `render` override builds the whole tree, otherwise the default container lists
2506
+ * the items and appends the controls (all five for a known total, prev/indicator/next for a cursor source).
2507
+ */
2508
+ function renderPage(view, cursor, config) {
2509
+ const controls = new Controls(cursor, view);
2510
+ if (config.render) return toReplyResponse(config.render(view, controls));
2511
+ return { components: [new PageContainer(view, config.renderItem ?? ((item) => String(item)), controls.row(...view.totalPages === void 0 ? CURSOR_KEYS : ARRAY_KEYS)).component] };
2512
+ }
2513
+
2514
+ //#endregion
2515
+ //#region src/pagination/Paginator.ts
2516
+ function contextOf(interaction, core) {
2517
+ return {
2518
+ interaction,
2519
+ user: interaction.user,
2520
+ guild: interaction.guild,
2521
+ ...core && { core }
2522
+ };
2523
+ }
2524
+ /**
2525
+ * A restart-proof paginator. Each nav button's customId encodes its full target page, so clicks are
2526
+ * idempotent and survive a restart. A persistent `@ButtonRoute` on `Handler` dispatches the clicks.
2527
+ *
2528
+ * @typeParam Item - The item type, inferred from the source.
2529
+ * @typeParam Prefix - The route prefix, inferred from `config.prefix`.
2530
+ *
2531
+ * @example
2532
+ * ```ts
2533
+ * export const Bans = new Paginator({
2534
+ * prefix: 'bans',
2535
+ * source: new ArraySource((ctx) => ctx.guild.bans.fetch().then((b) => [...b.values()]), { perPage: 10 }),
2536
+ * renderItem: (ban) => ban.user.tag
2537
+ * });
2538
+ *
2539
+ * \@ButtonRoute(Bans.cursor)
2540
+ * export class BansNav extends Bans.Handler {}
2541
+ * ```
2542
+ */
2543
+ var Paginator = class {
2544
+ /** The page cursor, pass it to your `@ButtonRoute`. */
2545
+ cursor;
2546
+ /** The nav handler base. Extend it with an empty body and decorate it, `@ButtonRoute(p.cursor)`. */
2547
+ Handler;
2548
+ config;
2549
+ constructor(config) {
2550
+ this.cursor = (0, _seedcord_kit_internal.pageCursor)(config.prefix);
2551
+ this.config = config;
2552
+ const loadPage = (ctx, n) => this.page(ctx, n);
2553
+ this.Handler = class Nav extends ButtonHandler {
2554
+ async execute() {
2555
+ await this.event.deferUpdate();
2556
+ const response = await loadPage(contextOf(this.event, this.core), this.params.page);
2557
+ await new ReplySender(this.event).edit(this.event.message, response);
2558
+ }
2559
+ };
2560
+ }
2561
+ /** Render page 0 and send it, picking reply or followUp from the interaction's state. */
2562
+ async start(interaction, core) {
2563
+ const response = await this.page(contextOf(interaction, core), 0);
2564
+ return new ReplySender(interaction).send(response, this.config.ephemeral ?? false);
2565
+ }
2566
+ /** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
2567
+ async page(ctx, n) {
2568
+ return renderPage(await this.config.source.page(ctx, n), this.cursor, this.config);
2569
+ }
2570
+ };
2571
+
2572
+ //#endregion
2573
+ //#region src/pagination/sources.ts
2574
+ const DEFAULT_PER_PAGE = 10;
2575
+ /**
2576
+ * A source for a bounded list you can load whole. It loads the full list on every click, then slices the
2577
+ * page via {@link paginate}, so the real total is known (a real last button, "Page X of Y"). Cache inside
2578
+ * your loader if the load is expensive.
2579
+ *
2580
+ * @typeParam Item - The item type, inferred from the loader's return.
2581
+ */
2582
+ var ArraySource = class {
2583
+ load;
2584
+ perPage;
2585
+ constructor(load, opts) {
2586
+ this.load = load;
2587
+ this.perPage = opts?.perPage ?? DEFAULT_PER_PAGE;
2588
+ if (!Number.isInteger(this.perPage) || this.perPage <= 0) throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.PaginationInvalidPerPage, [this.perPage]);
2589
+ }
2590
+ async page(ctx, n) {
2591
+ return (0, _seedcord_kit.paginate)(await this.load(ctx), n, this.perPage);
2592
+ }
2593
+ };
2594
+ /**
2595
+ * A source for a large or unknown-length set you fetch one page at a time (SQL LIMIT/OFFSET, a paged API).
2596
+ * The fetcher receives the page index and the page size and reports whether a next page exists. A cursor
2597
+ * source has no cheap total, so `totalPages` is undefined, the last button is omitted, and the indicator
2598
+ * reads "Page X".
2599
+ *
2600
+ * @typeParam Item - The item type, inferred from the fetcher's slice.
2601
+ */
2602
+ var CursorSource = class {
2603
+ fetch;
2604
+ perPage;
2605
+ constructor(fetch, opts) {
2606
+ this.fetch = fetch;
2607
+ this.perPage = opts?.perPage ?? DEFAULT_PER_PAGE;
2608
+ if (!Number.isInteger(this.perPage) || this.perPage <= 0) throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.PaginationInvalidPerPage, [this.perPage]);
2609
+ }
2610
+ async page(ctx, n) {
2611
+ const page = Math.max(0, Math.trunc(n));
2612
+ const { items, hasNext } = await this.fetch(ctx, page, this.perPage);
2613
+ return {
2614
+ items: [...items],
2615
+ page,
2616
+ perPage: this.perPage,
2617
+ hasPrev: page > 0,
2618
+ hasNext
2619
+ };
2620
+ }
2621
+ };
2622
+
2398
2623
  //#endregion
2399
2624
  //#region src/subscribers/Subscriber.ts
2400
2625
  /**
@@ -4125,9 +4350,10 @@ var Seedcord = class Seedcord extends Pluggable {
4125
4350
  //#endregion
4126
4351
  //#region src/index.ts
4127
4352
  /** Package version */
4128
- const version = "0.14.0";
4353
+ const version = "0.15.0-next.1";
4129
4354
 
4130
4355
  //#endregion
4356
+ exports.ArraySource = ArraySource;
4131
4357
  exports.AutocompleteHandler = AutocompleteHandler;
4132
4358
  exports.AutocompleteRoute = AutocompleteRoute;
4133
4359
  exports.ButtonHandler = ButtonHandler;
@@ -4136,6 +4362,7 @@ exports.CommandMentions = CommandMentions;
4136
4362
  exports.ContextMenuHandler = ContextMenuHandler;
4137
4363
  exports.ContextMenuRoute = ContextMenuRoute;
4138
4364
  exports.Cooldown = Cooldown;
4365
+ exports.CursorSource = CursorSource;
4139
4366
  exports.DmOnly = DmOnly;
4140
4367
  exports.Emojis = Emojis;
4141
4368
  exports.EventHandler = EventHandler;
@@ -4152,6 +4379,7 @@ exports.ModalHandler = ModalHandler;
4152
4379
  exports.ModalRoute = ModalRoute;
4153
4380
  exports.Nsfw = Nsfw;
4154
4381
  exports.OwnerOnly = OwnerOnly;
4382
+ exports.Paginator = Paginator;
4155
4383
  exports.Pluggable = Pluggable;
4156
4384
  exports.Plugin = Plugin;
4157
4385
  exports.RegisterCommand = RegisterCommand;
@@ -4161,7 +4389,7 @@ exports.RequireBotPermissions = RequireBotPermissions;
4161
4389
  exports.RequirePermissions = RequirePermissions;
4162
4390
  exports.RequireRole = RequireRole;
4163
4391
  exports.Seedcord = Seedcord;
4164
- exports.SelectHandler = SelectHandler;
4392
+ exports.SelectMenuHandler = SelectMenuHandler;
4165
4393
  exports.SelectMenuKind = SelectMenuKind;
4166
4394
  exports.SelectMenuRoute = SelectMenuRoute;
4167
4395
  exports.SlashHandler = SlashHandler;