@seedcord/kit 0.1.1 → 0.2.0-next.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.
@@ -310,5 +310,5 @@ type DecodedRoute<Defs extends readonly AnyCustomId[]> = { [Index in keyof Defs]
310
310
  */
311
311
  declare function decodeFor<Defs extends readonly AnyCustomId[]>(defs: Defs, wire: string): DecodedRoute<Defs>;
312
312
  //#endregion
313
- export { DecodedParams as a, BuilderType as c, prefixOf as i, RowType as l, CustomId as n, BuilderComponent as o, decodeFor as r, RowComponent as s, AnyCustomId as t };
314
- //# sourceMappingURL=CustomId-TT4MsOsD.d.mts.map
313
+ export { CustomIdField as a, RowComponent as c, prefixOf as i, BuilderType as l, CustomId as n, DecodedParams as o, decodeFor as r, BuilderComponent as s, AnyCustomId as t, RowType as u };
314
+ //# sourceMappingURL=CustomId-CtpiL97C.d.mts.map
package/dist/index.cjs CHANGED
@@ -1,5 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_CustomId = require('./CustomId-NsInJKhD.cjs');
3
+ let _seedcord_errors = require("@seedcord/errors");
4
+ let _seedcord_errors_internal = require("@seedcord/errors/internal");
3
5
 
4
6
  //#region src/stops/Fault.ts
5
7
  /**
@@ -68,10 +70,36 @@ var Silence = class extends Error {
68
70
  }
69
71
  };
70
72
 
73
+ //#endregion
74
+ //#region src/pagination/paginate.ts
75
+ /**
76
+ * Pure page math, usable headless. Clamps `page` into range and slices the window.
77
+ *
78
+ * @param items - The full list to page over.
79
+ * @param page - The requested zero-based page, clamped into `[0, totalPages - 1]`.
80
+ * @param perPage - The page size. Must be a positive integer.
81
+ * @returns The sliced {@link PageView}, with `totalPages` always known.
82
+ * @throws SeedcordRangeError when `perPage` is not a positive integer.
83
+ */
84
+ function paginate(items, page, perPage) {
85
+ if (!Number.isInteger(perPage) || perPage <= 0) throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.PaginationInvalidPerPage, [perPage]);
86
+ const totalPages = Math.max(1, Math.ceil(items.length / perPage));
87
+ const clamped = Math.min(Math.max(Math.trunc(page), 0), totalPages - 1);
88
+ const start = clamped * perPage;
89
+ return {
90
+ items: items.slice(start, start + perPage),
91
+ page: clamped,
92
+ perPage,
93
+ totalPages,
94
+ hasPrev: clamped > 0,
95
+ hasNext: clamped < totalPages - 1
96
+ };
97
+ }
98
+
71
99
  //#endregion
72
100
  //#region src/index.ts
73
101
  /** Package version */
74
- const version = "0.1.1";
102
+ const version = "0.2.0-next.0";
75
103
 
76
104
  //#endregion
77
105
  exports.BuilderComponent = require_CustomId.BuilderComponent;
@@ -80,5 +108,6 @@ exports.Fault = Fault;
80
108
  exports.Notice = require_CustomId.Notice;
81
109
  exports.RowComponent = require_CustomId.RowComponent;
82
110
  exports.Silence = Silence;
111
+ exports.paginate = paginate;
83
112
  exports.version = version;
84
113
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["Notice","NoticeCard"],"sources":["../src/stops/Fault.ts","../src/stops/Silence.ts","../src/index.ts"],"sourcesContent":["import { Notice } from './Notice';\nimport { NoticeCard } from './NoticeCard';\n\nimport type { RenderContext, ReplyResponse } from '@seedcord/types';\n\n/**\n * A generic fault you throw after catching an error you do not have a specific message for.\n *\n * The user sees a fixed generic reply with the tracking uuid, never the cause. `report` defaults to\n * `true`, so the framework logs it and publishes it to the `handledException` bus. Pass `report: false`\n * to show the generic reply without the bus publish. The original error is stored as the standard\n * `cause`, so the real stack reaches the webhook. For a fault the user should see a real message,\n * subclass {@link Notice} and write your own `render` instead.\n *\n * The framework also renders this for an unhandled throw, where it points the user at\n * `ctx.developerUsername`.\n *\n * @example\n * ```ts\n * import { Fault } from '@seedcord/kit';\n *\n * try {\n * await db.write(record);\n * } catch (cause) {\n * // user sees the generic reply with the uuid, the real error rides along as cause for the webhook\n * throw new Fault({ cause });\n *\n * // pass report: false to show the same reply without publishing to the handledException bus\n * // throw new Fault({ cause, report: false });\n * }\n * ```\n */\nexport class Fault extends Notice {\n public constructor(options?: { cause?: unknown; report?: boolean }) {\n super('A fault occurred', options?.cause === undefined ? undefined : { cause: options.cause });\n this.report = options?.report ?? true;\n }\n\n public render(ctx: RenderContext): ReplyResponse {\n const contact = ctx.developerUsername ?? 'the developer';\n const card = new NoticeCard(\n `Something went wrong. Please reach out to ${contact} with a way to reproduce the error and the following:\\n### UUID: \\`${ctx.uuid}\\``,\n 'Error'\n );\n return { components: [card.component] };\n }\n}\n","/**\n * Throw to stop a handler with no reply and no report.\n *\n * The framework boundary catches `Silence` before {@link Notice}, makes zero Discord calls, and stops.\n * Ideally you'd only throw this in `EventHandlers` (or `Gates` for those), because it doesn't make sense to\n * leave the user with no reply for an interaction.\n *\n * @example\n * ```ts\n * import { Silence } from '@seedcord/kit';\n *\n * // before any reply or defer, drop the interaction with no reply and no report\n * if (await isBlacklisted(interaction.user.id)) throw new Silence('blacklisted user');\n * ```\n */\nexport class Silence extends Error {\n /**\n * @param reason - Optional note written only to a debug log, never shown to the user or reported.\n */\n public constructor(public readonly reason?: string) {\n super(reason ?? 'Silence');\n\n Error.captureStackTrace(this, this.constructor);\n }\n}\n","export { BuilderComponent, RowComponent } from '@components/Component';\nexport { type RowType, type BuilderType } from '@components/builderTypes';\nexport { Notice } from '@stops/Notice';\nexport { Fault } from '@stops/Fault';\nexport { Silence } from '@stops/Silence';\nexport { CustomId } from '@customId/CustomId';\n\n/** Package version */\nexport const version = process.env.PACKAGE_VERSION ?? '0.0.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,QAAb,cAA2BA,wBAAO;CAC9B,AAAO,YAAY,SAAiD;EAChE,MAAM,oBAAoB,SAAS,UAAU,SAAY,SAAY,EAAE,OAAO,QAAQ,MAAM,CAAC;EAC7F,KAAK,SAAS,SAAS,UAAU;CACrC;CAEA,AAAO,OAAO,KAAmC;EAM7C,OAAO,EAAE,YAAY,CAAC,IAJLC,4BACb,6CAFY,IAAI,qBAAqB,gBAEgB,qEAAqE,IAAI,KAAK,KACnI,OAEqB,CAAC,CAAC,SAAS,EAAE;CAC1C;AACJ;;;;;;;;;;;;;;;;;;;AC/BA,IAAa,UAAb,cAA6B,MAAM;CAII;;;;CAAnC,AAAO,YAAY,AAAgB,QAAiB;EAChD,MAAM,UAAU,SAAS;EADM;EAG/B,MAAM,kBAAkB,MAAM,KAAK,WAAW;CAClD;AACJ;;;;;AChBA,MAAa"}
1
+ {"version":3,"file":"index.cjs","names":["Notice","NoticeCard","SeedcordRangeError","SeedcordErrorCode"],"sources":["../src/stops/Fault.ts","../src/stops/Silence.ts","../src/pagination/paginate.ts","../src/index.ts"],"sourcesContent":["import { Notice } from './Notice';\nimport { NoticeCard } from './NoticeCard';\n\nimport type { RenderContext, ReplyResponse } from '@seedcord/types';\n\n/**\n * A generic fault you throw after catching an error you do not have a specific message for.\n *\n * The user sees a fixed generic reply with the tracking uuid, never the cause. `report` defaults to\n * `true`, so the framework logs it and publishes it to the `handledException` bus. Pass `report: false`\n * to show the generic reply without the bus publish. The original error is stored as the standard\n * `cause`, so the real stack reaches the webhook. For a fault the user should see a real message,\n * subclass {@link Notice} and write your own `render` instead.\n *\n * The framework also renders this for an unhandled throw, where it points the user at\n * `ctx.developerUsername`.\n *\n * @example\n * ```ts\n * import { Fault } from '@seedcord/kit';\n *\n * try {\n * await db.write(record);\n * } catch (cause) {\n * // user sees the generic reply with the uuid, the real error rides along as cause for the webhook\n * throw new Fault({ cause });\n *\n * // pass report: false to show the same reply without publishing to the handledException bus\n * // throw new Fault({ cause, report: false });\n * }\n * ```\n */\nexport class Fault extends Notice {\n public constructor(options?: { cause?: unknown; report?: boolean }) {\n super('A fault occurred', options?.cause === undefined ? undefined : { cause: options.cause });\n this.report = options?.report ?? true;\n }\n\n public render(ctx: RenderContext): ReplyResponse {\n const contact = ctx.developerUsername ?? 'the developer';\n const card = new NoticeCard(\n `Something went wrong. Please reach out to ${contact} with a way to reproduce the error and the following:\\n### UUID: \\`${ctx.uuid}\\``,\n 'Error'\n );\n return { components: [card.component] };\n }\n}\n","/**\n * Throw to stop a handler with no reply and no report.\n *\n * The framework boundary catches `Silence` before {@link Notice}, makes zero Discord calls, and stops.\n * Ideally you'd only throw this in `EventHandlers` (or `Gates` for those), because it doesn't make sense to\n * leave the user with no reply for an interaction.\n *\n * @example\n * ```ts\n * import { Silence } from '@seedcord/kit';\n *\n * // before any reply or defer, drop the interaction with no reply and no report\n * if (await isBlacklisted(interaction.user.id)) throw new Silence('blacklisted user');\n * ```\n */\nexport class Silence extends Error {\n /**\n * @param reason - Optional note written only to a debug log, never shown to the user or reported.\n */\n public constructor(public readonly reason?: string) {\n super(reason ?? 'Silence');\n\n Error.captureStackTrace(this, this.constructor);\n }\n}\n","import { SeedcordErrorCode } from '@seedcord/errors';\nimport { SeedcordRangeError } from '@seedcord/errors/internal';\n\nimport type { PageView } from './PageView';\n\n/**\n * Pure page math, usable headless. Clamps `page` into range and slices the window.\n *\n * @param items - The full list to page over.\n * @param page - The requested zero-based page, clamped into `[0, totalPages - 1]`.\n * @param perPage - The page size. Must be a positive integer.\n * @returns The sliced {@link PageView}, with `totalPages` always known.\n * @throws SeedcordRangeError when `perPage` is not a positive integer.\n */\nexport function paginate<Item>(items: readonly Item[], page: number, perPage: number): PageView<Item> {\n if (!Number.isInteger(perPage) || perPage <= 0) {\n throw new SeedcordRangeError(SeedcordErrorCode.PaginationInvalidPerPage, [perPage]);\n }\n\n const totalPages = Math.max(1, Math.ceil(items.length / perPage));\n // a headless caller can pass a non-integer or out-of-range page.\n const clamped = Math.min(Math.max(Math.trunc(page), 0), totalPages - 1);\n const start = clamped * perPage;\n\n return {\n items: items.slice(start, start + perPage),\n page: clamped,\n perPage,\n totalPages,\n hasPrev: clamped > 0,\n hasNext: clamped < totalPages - 1\n };\n}\n","export { BuilderComponent, RowComponent } from '@components/Component';\nexport { type RowType, type BuilderType } from '@components/builderTypes';\nexport { Notice } from '@stops/Notice';\nexport { Fault } from '@stops/Fault';\nexport { Silence } from '@stops/Silence';\nexport { CustomId } from '@customId/CustomId';\nexport { paginate } from '@pagination/paginate';\nexport { type PageView } from '@pagination/PageView';\n\n/** Package version */\nexport const version = process.env.PACKAGE_VERSION ?? '0.0.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,QAAb,cAA2BA,wBAAO;CAC9B,AAAO,YAAY,SAAiD;EAChE,MAAM,oBAAoB,SAAS,UAAU,SAAY,SAAY,EAAE,OAAO,QAAQ,MAAM,CAAC;EAC7F,KAAK,SAAS,SAAS,UAAU;CACrC;CAEA,AAAO,OAAO,KAAmC;EAM7C,OAAO,EAAE,YAAY,CAAC,IAJLC,4BACb,6CAFY,IAAI,qBAAqB,gBAEgB,qEAAqE,IAAI,KAAK,KACnI,OAEqB,CAAC,CAAC,SAAS,EAAE;CAC1C;AACJ;;;;;;;;;;;;;;;;;;;AC/BA,IAAa,UAAb,cAA6B,MAAM;CAII;;;;CAAnC,AAAO,YAAY,AAAgB,QAAiB;EAChD,MAAM,UAAU,SAAS;EADM;EAG/B,MAAM,kBAAkB,MAAM,KAAK,WAAW;CAClD;AACJ;;;;;;;;;;;;;ACVA,SAAgB,SAAe,OAAwB,MAAc,SAAiC;CAClG,IAAI,CAAC,OAAO,UAAU,OAAO,KAAK,WAAW,GACzC,MAAM,IAAIC,6CAAmBC,mCAAkB,0BAA0B,CAAC,OAAO,CAAC;CAGtF,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,KAAK,MAAM,SAAS,OAAO,CAAC;CAEhE,MAAM,UAAU,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,IAAI,GAAG,CAAC,GAAG,aAAa,CAAC;CACtE,MAAM,QAAQ,UAAU;CAExB,OAAO;EACH,OAAO,MAAM,MAAM,OAAO,QAAQ,OAAO;EACzC,MAAM;EACN;EACA;EACA,SAAS,UAAU;EACnB,SAAS,UAAU,aAAa;CACpC;AACJ;;;;;ACtBA,MAAa"}
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { c as BuilderType, l as RowType, n as CustomId, o as BuilderComponent, s as RowComponent } from "./CustomId-TT4MsOsD.mjs";
1
+ import { c as RowComponent, l as BuilderType, n as CustomId, s as BuilderComponent, u as RowType } from "./CustomId-CtpiL97C.mjs";
2
2
  import { RenderContext, ReplyResponse } from "@seedcord/types";
3
3
 
4
4
  //#region src/stops/Notice.d.ts
@@ -127,9 +127,38 @@ declare class Silence extends Error {
127
127
  constructor(reason?: string | undefined);
128
128
  }
129
129
  //#endregion
130
+ //#region src/pagination/PageView.d.ts
131
+ /**
132
+ * One rendered page of a paginated list.
133
+ *
134
+ * @typeParam Item - The item type.
135
+ */
136
+ interface PageView<Item> {
137
+ items: Item[];
138
+ /** Zero-based, already clamped into range. */
139
+ page: number;
140
+ perPage: number;
141
+ /** The total page count, or `undefined` for a cursor source that has no cheap total. */
142
+ totalPages?: number;
143
+ hasPrev: boolean;
144
+ hasNext: boolean;
145
+ }
146
+ //#endregion
147
+ //#region src/pagination/paginate.d.ts
148
+ /**
149
+ * Pure page math, usable headless. Clamps `page` into range and slices the window.
150
+ *
151
+ * @param items - The full list to page over.
152
+ * @param page - The requested zero-based page, clamped into `[0, totalPages - 1]`.
153
+ * @param perPage - The page size. Must be a positive integer.
154
+ * @returns The sliced {@link PageView}, with `totalPages` always known.
155
+ * @throws SeedcordRangeError when `perPage` is not a positive integer.
156
+ */
157
+ declare function paginate<Item>(items: readonly Item[], page: number, perPage: number): PageView<Item>;
158
+ //#endregion
130
159
  //#region src/index.d.ts
131
160
  /** Package version */
132
161
  declare const version: string;
133
162
  //#endregion
134
- export { BuilderComponent, type BuilderType, CustomId, Fault, Notice, RowComponent, type RowType, Silence, version };
163
+ export { BuilderComponent, type BuilderType, CustomId, Fault, Notice, type PageView, RowComponent, type RowType, Silence, paginate, version };
135
164
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  import { a as Notice, i as NoticeCard, o as BuilderComponent, s as RowComponent, t as CustomId } from "./CustomId-BKioviDw.mjs";
2
+ import { SeedcordErrorCode } from "@seedcord/errors";
3
+ import { SeedcordRangeError } from "@seedcord/errors/internal";
2
4
 
3
5
  //#region src/stops/Fault.ts
4
6
  /**
@@ -67,11 +69,37 @@ var Silence = class extends Error {
67
69
  }
68
70
  };
69
71
 
72
+ //#endregion
73
+ //#region src/pagination/paginate.ts
74
+ /**
75
+ * Pure page math, usable headless. Clamps `page` into range and slices the window.
76
+ *
77
+ * @param items - The full list to page over.
78
+ * @param page - The requested zero-based page, clamped into `[0, totalPages - 1]`.
79
+ * @param perPage - The page size. Must be a positive integer.
80
+ * @returns The sliced {@link PageView}, with `totalPages` always known.
81
+ * @throws SeedcordRangeError when `perPage` is not a positive integer.
82
+ */
83
+ function paginate(items, page, perPage) {
84
+ if (!Number.isInteger(perPage) || perPage <= 0) throw new SeedcordRangeError(SeedcordErrorCode.PaginationInvalidPerPage, [perPage]);
85
+ const totalPages = Math.max(1, Math.ceil(items.length / perPage));
86
+ const clamped = Math.min(Math.max(Math.trunc(page), 0), totalPages - 1);
87
+ const start = clamped * perPage;
88
+ return {
89
+ items: items.slice(start, start + perPage),
90
+ page: clamped,
91
+ perPage,
92
+ totalPages,
93
+ hasPrev: clamped > 0,
94
+ hasNext: clamped < totalPages - 1
95
+ };
96
+ }
97
+
70
98
  //#endregion
71
99
  //#region src/index.ts
72
100
  /** Package version */
73
- const version = "0.1.1";
101
+ const version = "0.2.0-next.0";
74
102
 
75
103
  //#endregion
76
- export { BuilderComponent, CustomId, Fault, Notice, RowComponent, Silence, version };
104
+ export { BuilderComponent, CustomId, Fault, Notice, RowComponent, Silence, paginate, version };
77
105
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/stops/Fault.ts","../src/stops/Silence.ts","../src/index.ts"],"sourcesContent":["import { Notice } from './Notice';\nimport { NoticeCard } from './NoticeCard';\n\nimport type { RenderContext, ReplyResponse } from '@seedcord/types';\n\n/**\n * A generic fault you throw after catching an error you do not have a specific message for.\n *\n * The user sees a fixed generic reply with the tracking uuid, never the cause. `report` defaults to\n * `true`, so the framework logs it and publishes it to the `handledException` bus. Pass `report: false`\n * to show the generic reply without the bus publish. The original error is stored as the standard\n * `cause`, so the real stack reaches the webhook. For a fault the user should see a real message,\n * subclass {@link Notice} and write your own `render` instead.\n *\n * The framework also renders this for an unhandled throw, where it points the user at\n * `ctx.developerUsername`.\n *\n * @example\n * ```ts\n * import { Fault } from '@seedcord/kit';\n *\n * try {\n * await db.write(record);\n * } catch (cause) {\n * // user sees the generic reply with the uuid, the real error rides along as cause for the webhook\n * throw new Fault({ cause });\n *\n * // pass report: false to show the same reply without publishing to the handledException bus\n * // throw new Fault({ cause, report: false });\n * }\n * ```\n */\nexport class Fault extends Notice {\n public constructor(options?: { cause?: unknown; report?: boolean }) {\n super('A fault occurred', options?.cause === undefined ? undefined : { cause: options.cause });\n this.report = options?.report ?? true;\n }\n\n public render(ctx: RenderContext): ReplyResponse {\n const contact = ctx.developerUsername ?? 'the developer';\n const card = new NoticeCard(\n `Something went wrong. Please reach out to ${contact} with a way to reproduce the error and the following:\\n### UUID: \\`${ctx.uuid}\\``,\n 'Error'\n );\n return { components: [card.component] };\n }\n}\n","/**\n * Throw to stop a handler with no reply and no report.\n *\n * The framework boundary catches `Silence` before {@link Notice}, makes zero Discord calls, and stops.\n * Ideally you'd only throw this in `EventHandlers` (or `Gates` for those), because it doesn't make sense to\n * leave the user with no reply for an interaction.\n *\n * @example\n * ```ts\n * import { Silence } from '@seedcord/kit';\n *\n * // before any reply or defer, drop the interaction with no reply and no report\n * if (await isBlacklisted(interaction.user.id)) throw new Silence('blacklisted user');\n * ```\n */\nexport class Silence extends Error {\n /**\n * @param reason - Optional note written only to a debug log, never shown to the user or reported.\n */\n public constructor(public readonly reason?: string) {\n super(reason ?? 'Silence');\n\n Error.captureStackTrace(this, this.constructor);\n }\n}\n","export { BuilderComponent, RowComponent } from '@components/Component';\nexport { type RowType, type BuilderType } from '@components/builderTypes';\nexport { Notice } from '@stops/Notice';\nexport { Fault } from '@stops/Fault';\nexport { Silence } from '@stops/Silence';\nexport { CustomId } from '@customId/CustomId';\n\n/** Package version */\nexport const version = process.env.PACKAGE_VERSION ?? '0.0.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,QAAb,cAA2B,OAAO;CAC9B,AAAO,YAAY,SAAiD;EAChE,MAAM,oBAAoB,SAAS,UAAU,SAAY,SAAY,EAAE,OAAO,QAAQ,MAAM,CAAC;EAC7F,KAAK,SAAS,SAAS,UAAU;CACrC;CAEA,AAAO,OAAO,KAAmC;EAM7C,OAAO,EAAE,YAAY,CAAC,IAJL,WACb,6CAFY,IAAI,qBAAqB,gBAEgB,qEAAqE,IAAI,KAAK,KACnI,OAEqB,CAAC,CAAC,SAAS,EAAE;CAC1C;AACJ;;;;;;;;;;;;;;;;;;;AC/BA,IAAa,UAAb,cAA6B,MAAM;CAII;;;;CAAnC,AAAO,YAAY,AAAgB,QAAiB;EAChD,MAAM,UAAU,SAAS;EADM;EAG/B,MAAM,kBAAkB,MAAM,KAAK,WAAW;CAClD;AACJ;;;;;AChBA,MAAa"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/stops/Fault.ts","../src/stops/Silence.ts","../src/pagination/paginate.ts","../src/index.ts"],"sourcesContent":["import { Notice } from './Notice';\nimport { NoticeCard } from './NoticeCard';\n\nimport type { RenderContext, ReplyResponse } from '@seedcord/types';\n\n/**\n * A generic fault you throw after catching an error you do not have a specific message for.\n *\n * The user sees a fixed generic reply with the tracking uuid, never the cause. `report` defaults to\n * `true`, so the framework logs it and publishes it to the `handledException` bus. Pass `report: false`\n * to show the generic reply without the bus publish. The original error is stored as the standard\n * `cause`, so the real stack reaches the webhook. For a fault the user should see a real message,\n * subclass {@link Notice} and write your own `render` instead.\n *\n * The framework also renders this for an unhandled throw, where it points the user at\n * `ctx.developerUsername`.\n *\n * @example\n * ```ts\n * import { Fault } from '@seedcord/kit';\n *\n * try {\n * await db.write(record);\n * } catch (cause) {\n * // user sees the generic reply with the uuid, the real error rides along as cause for the webhook\n * throw new Fault({ cause });\n *\n * // pass report: false to show the same reply without publishing to the handledException bus\n * // throw new Fault({ cause, report: false });\n * }\n * ```\n */\nexport class Fault extends Notice {\n public constructor(options?: { cause?: unknown; report?: boolean }) {\n super('A fault occurred', options?.cause === undefined ? undefined : { cause: options.cause });\n this.report = options?.report ?? true;\n }\n\n public render(ctx: RenderContext): ReplyResponse {\n const contact = ctx.developerUsername ?? 'the developer';\n const card = new NoticeCard(\n `Something went wrong. Please reach out to ${contact} with a way to reproduce the error and the following:\\n### UUID: \\`${ctx.uuid}\\``,\n 'Error'\n );\n return { components: [card.component] };\n }\n}\n","/**\n * Throw to stop a handler with no reply and no report.\n *\n * The framework boundary catches `Silence` before {@link Notice}, makes zero Discord calls, and stops.\n * Ideally you'd only throw this in `EventHandlers` (or `Gates` for those), because it doesn't make sense to\n * leave the user with no reply for an interaction.\n *\n * @example\n * ```ts\n * import { Silence } from '@seedcord/kit';\n *\n * // before any reply or defer, drop the interaction with no reply and no report\n * if (await isBlacklisted(interaction.user.id)) throw new Silence('blacklisted user');\n * ```\n */\nexport class Silence extends Error {\n /**\n * @param reason - Optional note written only to a debug log, never shown to the user or reported.\n */\n public constructor(public readonly reason?: string) {\n super(reason ?? 'Silence');\n\n Error.captureStackTrace(this, this.constructor);\n }\n}\n","import { SeedcordErrorCode } from '@seedcord/errors';\nimport { SeedcordRangeError } from '@seedcord/errors/internal';\n\nimport type { PageView } from './PageView';\n\n/**\n * Pure page math, usable headless. Clamps `page` into range and slices the window.\n *\n * @param items - The full list to page over.\n * @param page - The requested zero-based page, clamped into `[0, totalPages - 1]`.\n * @param perPage - The page size. Must be a positive integer.\n * @returns The sliced {@link PageView}, with `totalPages` always known.\n * @throws SeedcordRangeError when `perPage` is not a positive integer.\n */\nexport function paginate<Item>(items: readonly Item[], page: number, perPage: number): PageView<Item> {\n if (!Number.isInteger(perPage) || perPage <= 0) {\n throw new SeedcordRangeError(SeedcordErrorCode.PaginationInvalidPerPage, [perPage]);\n }\n\n const totalPages = Math.max(1, Math.ceil(items.length / perPage));\n // a headless caller can pass a non-integer or out-of-range page.\n const clamped = Math.min(Math.max(Math.trunc(page), 0), totalPages - 1);\n const start = clamped * perPage;\n\n return {\n items: items.slice(start, start + perPage),\n page: clamped,\n perPage,\n totalPages,\n hasPrev: clamped > 0,\n hasNext: clamped < totalPages - 1\n };\n}\n","export { BuilderComponent, RowComponent } from '@components/Component';\nexport { type RowType, type BuilderType } from '@components/builderTypes';\nexport { Notice } from '@stops/Notice';\nexport { Fault } from '@stops/Fault';\nexport { Silence } from '@stops/Silence';\nexport { CustomId } from '@customId/CustomId';\nexport { paginate } from '@pagination/paginate';\nexport { type PageView } from '@pagination/PageView';\n\n/** Package version */\nexport const version = process.env.PACKAGE_VERSION ?? '0.0.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,QAAb,cAA2B,OAAO;CAC9B,AAAO,YAAY,SAAiD;EAChE,MAAM,oBAAoB,SAAS,UAAU,SAAY,SAAY,EAAE,OAAO,QAAQ,MAAM,CAAC;EAC7F,KAAK,SAAS,SAAS,UAAU;CACrC;CAEA,AAAO,OAAO,KAAmC;EAM7C,OAAO,EAAE,YAAY,CAAC,IAJL,WACb,6CAFY,IAAI,qBAAqB,gBAEgB,qEAAqE,IAAI,KAAK,KACnI,OAEqB,CAAC,CAAC,SAAS,EAAE;CAC1C;AACJ;;;;;;;;;;;;;;;;;;;AC/BA,IAAa,UAAb,cAA6B,MAAM;CAII;;;;CAAnC,AAAO,YAAY,AAAgB,QAAiB;EAChD,MAAM,UAAU,SAAS;EADM;EAG/B,MAAM,kBAAkB,MAAM,KAAK,WAAW;CAClD;AACJ;;;;;;;;;;;;;ACVA,SAAgB,SAAe,OAAwB,MAAc,SAAiC;CAClG,IAAI,CAAC,OAAO,UAAU,OAAO,KAAK,WAAW,GACzC,MAAM,IAAI,mBAAmB,kBAAkB,0BAA0B,CAAC,OAAO,CAAC;CAGtF,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,KAAK,MAAM,SAAS,OAAO,CAAC;CAEhE,MAAM,UAAU,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,IAAI,GAAG,CAAC,GAAG,aAAa,CAAC;CACtE,MAAM,QAAQ,UAAU;CAExB,OAAO;EACH,OAAO,MAAM,MAAM,OAAO,QAAQ,OAAO;EACzC,MAAM;EACN;EACA;EACA,SAAS,UAAU;EACnB,SAAS,UAAU,aAAa;CACpC;AACJ;;;;;ACtBA,MAAa"}
@@ -1,6 +1,15 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_CustomId = require('./CustomId-NsInJKhD.cjs');
3
3
 
4
+ //#region src/pagination/cursor.ts
5
+ const PAGE_MAX = 1e6;
6
+ const SLOT_MAX = 4;
7
+ /** Build the page cursor for a paginator. */
8
+ function pageCursor(prefix) {
9
+ return new require_CustomId.CustomId(prefix).int("page", 0, PAGE_MAX).int("slot", 0, 4);
10
+ }
11
+
12
+ //#endregion
4
13
  //#region src/customId/routing.ts
5
14
  /**
6
15
  * The route decorators store a handler's customId definitions here so the component base can decode
@@ -13,7 +22,9 @@ const ComponentDefsKey = Symbol("seedcord:customId:componentDefs");
13
22
  //#endregion
14
23
  exports.ComponentDefsKey = ComponentDefsKey;
15
24
  exports.NoticeCard = require_CustomId.NoticeCard;
25
+ exports.PAGE_MAX = PAGE_MAX;
16
26
  exports.decodeFor = require_CustomId.decodeFor;
27
+ exports.pageCursor = pageCursor;
17
28
  exports.prefixOf = require_CustomId.prefixOf;
18
29
  exports.setBotColor = require_CustomId.setBotColor;
19
30
  //# sourceMappingURL=internal.index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"internal.index.cjs","names":[],"sources":["../src/customId/routing.ts"],"sourcesContent":["import type { AnyCustomId } from './CustomId';\n\n/**\n * The route decorators store a handler's customId definitions here so the component base can decode\n * against them at runtime.\n *\n * @internal\n */\nexport const ComponentDefsKey = Symbol('seedcord:customId:componentDefs');\n\n/**\n * The phantom a component handler base carries. A route decorator constrains its argument to this, so\n * passing different definitions to the decorator and the handler's generic is a compile error. Never set at runtime.\n *\n * @internal\n */\nexport interface HasComponentDefs<Defs extends readonly AnyCustomId[]> {\n readonly __componentDefs?: Defs;\n}\n"],"mappings":";;;;;;;;;;AAQA,MAAa,mBAAmB,OAAO,iCAAiC"}
1
+ {"version":3,"file":"internal.index.cjs","names":["CustomId"],"sources":["../src/pagination/cursor.ts","../src/customId/routing.ts"],"sourcesContent":["import { CustomId } from '@customId/CustomId';\n\nimport type { CustomIdField } from '@customId/Field';\n\n// Included in the layout hash so it is fixed per prefix. Reducing it marks every active button as StaleCustomId.\n// @internal\nexport const PAGE_MAX = 1_000_000;\n\n// Discord rejects a message with duplicate custom_ids, and two controls can target the same page (first and\n// prev both hit 0), so each control uses a distinct slot.\n// @internal\nexport const SLOT_MAX = 4;\n\n/** Page cursor for a paginator. */\nexport type PageCursor<Prefix extends string> = CustomId<\n Prefix,\n { page: CustomIdField<number>; slot: CustomIdField<number> }\n>;\n\n/** Build the page cursor for a paginator. */\nexport function pageCursor<Prefix extends string>(prefix: Prefix): PageCursor<Prefix> {\n return new CustomId(prefix).int('page', 0, PAGE_MAX).int('slot', 0, SLOT_MAX);\n}\n","import type { AnyCustomId } from './CustomId';\n\n/**\n * The route decorators store a handler's customId definitions here so the component base can decode\n * against them at runtime.\n *\n * @internal\n */\nexport const ComponentDefsKey = Symbol('seedcord:customId:componentDefs');\n\n/**\n * The phantom a component handler base carries. A route decorator constrains its argument to this, so\n * passing different definitions to the decorator and the handler's generic is a compile error. Never set at runtime.\n *\n * @internal\n */\nexport interface HasComponentDefs<Defs extends readonly AnyCustomId[]> {\n readonly __componentDefs?: Defs;\n}\n"],"mappings":";;;;AAMA,MAAa,WAAW;AAKxB,MAAa,WAAW;;AASxB,SAAgB,WAAkC,QAAoC;CAClF,OAAO,IAAIA,0BAAS,MAAM,CAAC,CAAC,IAAI,QAAQ,GAAG,QAAQ,CAAC,CAAC,IAAI,QAAQ,IAAW;AAChF;;;;;;;;;;ACdA,MAAa,mBAAmB,OAAO,iCAAiC"}
@@ -1,10 +1,20 @@
1
- import { a as DecodedParams, i as prefixOf, o as BuilderComponent, r as decodeFor, t as AnyCustomId } from "./CustomId-TT4MsOsD.mjs";
1
+ import { a as CustomIdField, i as prefixOf, n as CustomId, o as DecodedParams, r as decodeFor, s as BuilderComponent, t as AnyCustomId } from "./CustomId-CtpiL97C.mjs";
2
2
  import { ColorResolvable } from "discord.js";
3
3
 
4
4
  //#region src/botColorHolder.d.ts
5
5
  /** @internal */
6
6
  declare function setBotColor(color: ColorResolvable | undefined): void;
7
7
  //#endregion
8
+ //#region src/pagination/cursor.d.ts
9
+ declare const PAGE_MAX = 1000000;
10
+ /** Page cursor for a paginator. */
11
+ type PageCursor<Prefix extends string> = CustomId<Prefix, {
12
+ page: CustomIdField<number>;
13
+ slot: CustomIdField<number>;
14
+ }>;
15
+ /** Build the page cursor for a paginator. */
16
+ declare function pageCursor<Prefix extends string>(prefix: Prefix): PageCursor<Prefix>;
17
+ //#endregion
8
18
  //#region src/customId/routing.d.ts
9
19
  /**
10
20
  * The route decorators store a handler's customId definitions here so the component base can decode
@@ -32,5 +42,5 @@ declare class NoticeCard extends BuilderComponent<'container'> {
32
42
  constructor(description: string, title?: string);
33
43
  }
34
44
  //#endregion
35
- export { type AnyCustomId, ComponentDefsKey, type DecodedParams, type HasComponentDefs, NoticeCard, decodeFor, prefixOf, setBotColor };
45
+ export { type AnyCustomId, ComponentDefsKey, type DecodedParams, type HasComponentDefs, NoticeCard, PAGE_MAX, type PageCursor, decodeFor, pageCursor, prefixOf, setBotColor };
36
46
  //# sourceMappingURL=internal.index.d.mts.map
@@ -1,5 +1,14 @@
1
- import { c as setBotColor, i as NoticeCard, n as decodeFor, r as prefixOf } from "./CustomId-BKioviDw.mjs";
1
+ import { c as setBotColor, i as NoticeCard, n as decodeFor, r as prefixOf, t as CustomId } from "./CustomId-BKioviDw.mjs";
2
2
 
3
+ //#region src/pagination/cursor.ts
4
+ const PAGE_MAX = 1e6;
5
+ const SLOT_MAX = 4;
6
+ /** Build the page cursor for a paginator. */
7
+ function pageCursor(prefix) {
8
+ return new CustomId(prefix).int("page", 0, PAGE_MAX).int("slot", 0, 4);
9
+ }
10
+
11
+ //#endregion
3
12
  //#region src/customId/routing.ts
4
13
  /**
5
14
  * The route decorators store a handler's customId definitions here so the component base can decode
@@ -10,5 +19,5 @@ import { c as setBotColor, i as NoticeCard, n as decodeFor, r as prefixOf } from
10
19
  const ComponentDefsKey = Symbol("seedcord:customId:componentDefs");
11
20
 
12
21
  //#endregion
13
- export { ComponentDefsKey, NoticeCard, decodeFor, prefixOf, setBotColor };
22
+ export { ComponentDefsKey, NoticeCard, PAGE_MAX, decodeFor, pageCursor, prefixOf, setBotColor };
14
23
  //# sourceMappingURL=internal.index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"internal.index.mjs","names":[],"sources":["../src/customId/routing.ts"],"sourcesContent":["import type { AnyCustomId } from './CustomId';\n\n/**\n * The route decorators store a handler's customId definitions here so the component base can decode\n * against them at runtime.\n *\n * @internal\n */\nexport const ComponentDefsKey = Symbol('seedcord:customId:componentDefs');\n\n/**\n * The phantom a component handler base carries. A route decorator constrains its argument to this, so\n * passing different definitions to the decorator and the handler's generic is a compile error. Never set at runtime.\n *\n * @internal\n */\nexport interface HasComponentDefs<Defs extends readonly AnyCustomId[]> {\n readonly __componentDefs?: Defs;\n}\n"],"mappings":";;;;;;;;;AAQA,MAAa,mBAAmB,OAAO,iCAAiC"}
1
+ {"version":3,"file":"internal.index.mjs","names":[],"sources":["../src/pagination/cursor.ts","../src/customId/routing.ts"],"sourcesContent":["import { CustomId } from '@customId/CustomId';\n\nimport type { CustomIdField } from '@customId/Field';\n\n// Included in the layout hash so it is fixed per prefix. Reducing it marks every active button as StaleCustomId.\n// @internal\nexport const PAGE_MAX = 1_000_000;\n\n// Discord rejects a message with duplicate custom_ids, and two controls can target the same page (first and\n// prev both hit 0), so each control uses a distinct slot.\n// @internal\nexport const SLOT_MAX = 4;\n\n/** Page cursor for a paginator. */\nexport type PageCursor<Prefix extends string> = CustomId<\n Prefix,\n { page: CustomIdField<number>; slot: CustomIdField<number> }\n>;\n\n/** Build the page cursor for a paginator. */\nexport function pageCursor<Prefix extends string>(prefix: Prefix): PageCursor<Prefix> {\n return new CustomId(prefix).int('page', 0, PAGE_MAX).int('slot', 0, SLOT_MAX);\n}\n","import type { AnyCustomId } from './CustomId';\n\n/**\n * The route decorators store a handler's customId definitions here so the component base can decode\n * against them at runtime.\n *\n * @internal\n */\nexport const ComponentDefsKey = Symbol('seedcord:customId:componentDefs');\n\n/**\n * The phantom a component handler base carries. A route decorator constrains its argument to this, so\n * passing different definitions to the decorator and the handler's generic is a compile error. Never set at runtime.\n *\n * @internal\n */\nexport interface HasComponentDefs<Defs extends readonly AnyCustomId[]> {\n readonly __componentDefs?: Defs;\n}\n"],"mappings":";;;AAMA,MAAa,WAAW;AAKxB,MAAa,WAAW;;AASxB,SAAgB,WAAkC,QAAoC;CAClF,OAAO,IAAI,SAAS,MAAM,CAAC,CAAC,IAAI,QAAQ,GAAG,QAAQ,CAAC,CAAC,IAAI,QAAQ,IAAW;AAChF;;;;;;;;;;ACdA,MAAa,mBAAmB,OAAO,iCAAiC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@seedcord/kit",
3
3
  "type": "module",
4
- "version": "0.1.1",
4
+ "version": "0.2.0-next.0",
5
5
  "description": "Reusable Seedcord building blocks: component builders, the Notice error tree, and the typed customId codec",
6
6
  "repository": {
7
7
  "type": "git",
@@ -42,15 +42,15 @@
42
42
  "typescript": "^6.0.3"
43
43
  },
44
44
  "dependencies": {
45
- "@seedcord/errors": "0.2.0",
45
+ "@seedcord/errors": "0.2.1-next.0",
46
46
  "@seedcord/types": "0.7.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "discord.js": "^14.26.4",
50
50
  "type-fest": "^5.7.0",
51
51
  "vite": "^8.0.16",
52
- "@seedcord/eslint-config": "1.4.2",
53
52
  "@seedcord/tsconfig": "2.0.0",
53
+ "@seedcord/eslint-config": "1.4.2",
54
54
  "@seedcord/tsdown-config": "2.0.0"
55
55
  },
56
56
  "publishConfig": {