@stambha/pagination 1.0.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/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/index.cjs +306 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +95 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +271 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mivaya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @stambha/pagination
|
|
2
|
+
|
|
3
|
+
**Embed pagination** with prev / next / dismiss buttons — built on Stambha **Signals** (`stambha:` custom ids).
|
|
4
|
+
|
|
5
|
+
Part of [**Stambha plugins**](https://github.com/Mivaya/Stambha-plugins) · requires [`@stambha/core`](https://github.com/Mivaya/Stambha) `^1.2.0`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @stambha/pagination @stambha/core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires **Node.js 20+**.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
Register the signal once (loader or manual register), then create a paginator from a command:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// src/signals/PaginationSignal.ts — or register in setup
|
|
25
|
+
import { PaginationSignal } from "@stambha/pagination";
|
|
26
|
+
// Piece loader discovers classes under src/signals/ when named accordingly.
|
|
27
|
+
export { PaginationSignal };
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { Command, type CommandContext, ok, type Registry } from "@stambha/core";
|
|
32
|
+
import { createPaginator } from "@stambha/pagination";
|
|
33
|
+
|
|
34
|
+
export class HelpPagesCommand extends Command {
|
|
35
|
+
constructor(registry: Registry<Command>) {
|
|
36
|
+
super(registry, {
|
|
37
|
+
name: "pages",
|
|
38
|
+
description: "Show a multi-page help embed",
|
|
39
|
+
kinds: ["slash", "prefix"],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async execute(ctx: CommandContext) {
|
|
44
|
+
const paginator = await createPaginator({
|
|
45
|
+
userId: ctx.userId,
|
|
46
|
+
pages: [
|
|
47
|
+
{ embeds: [{ title: "Page 1", description: "Getting started" }] },
|
|
48
|
+
{ embeds: [{ title: "Page 2", description: "Commands" }] },
|
|
49
|
+
{ embeds: [{ title: "Page 3", description: "Deployment" }] },
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await ctx.reply(paginator.message());
|
|
54
|
+
return ok(undefined);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Ensure `attachStambhaClient(hub, client, { signals: true })` (default) so button clicks route to `PaginationSignal`.
|
|
60
|
+
|
|
61
|
+
Custom ids look like:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
stambha:pagination:next:<sessionId>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Key exports
|
|
70
|
+
|
|
71
|
+
| Export | Purpose |
|
|
72
|
+
|--------|---------|
|
|
73
|
+
| `createPaginator` | Create a session and build the initial `ReplyPayload` |
|
|
74
|
+
| `PaginationSignal` | Handles prev / next / dismiss clicks |
|
|
75
|
+
| `Page`, `PaginatorOptions` | Types |
|
|
76
|
+
|
|
77
|
+
### Options
|
|
78
|
+
|
|
79
|
+
| Option | Default | Notes |
|
|
80
|
+
|--------|---------|--------|
|
|
81
|
+
| `pages` | required | Array or async resolver |
|
|
82
|
+
| `userId` | unset | Lock controls to the invoker |
|
|
83
|
+
| `timeoutMs` | `300000` | Session TTL (5 minutes) |
|
|
84
|
+
| `wrap` | `false` | Cycle at ends; otherwise disable prev/next |
|
|
85
|
+
| `labels` | Prev / Next / Dismiss | Button labels |
|
|
86
|
+
| `startAt` | `0` | Initial page index |
|
|
87
|
+
|
|
88
|
+
Page turns prefer Discord **UPDATE_MESSAGE** (interaction callback type 7) when `client.restPort` is set. Otherwise they fall back to `SignalContext.reply`.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Related packages
|
|
93
|
+
|
|
94
|
+
| Package | Role |
|
|
95
|
+
|---------|------|
|
|
96
|
+
| [`@stambha/core`](https://github.com/mivaya/Stambha/tree/main/packages/core) | Signals, `ReplyPayload`, client |
|
|
97
|
+
| [`@stambha/gateway`](https://github.com/mivaya/Stambha/tree/main/packages/gateway) | `attachStambhaClient` routing |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pnpm --filter @stambha/pagination build
|
|
105
|
+
pnpm --filter @stambha/pagination test
|
|
106
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
PAGINATION_SIGNAL_NAME: () => PAGINATION_SIGNAL_NAME,
|
|
24
|
+
PaginationSignal: () => PaginationSignal,
|
|
25
|
+
buildDismissPayload: () => buildDismissPayload,
|
|
26
|
+
buildPagePayload: () => buildPagePayload,
|
|
27
|
+
clearSessions: () => clearSessions,
|
|
28
|
+
createPaginator: () => createPaginator,
|
|
29
|
+
getSession: () => getSession,
|
|
30
|
+
paginationCustomId: () => paginationCustomId,
|
|
31
|
+
paginationSuffix: () => paginationSuffix,
|
|
32
|
+
parsePaginationSuffix: () => parsePaginationSuffix,
|
|
33
|
+
sessionCount: () => sessionCount
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/ids.ts
|
|
38
|
+
var PAGINATION_SIGNAL_NAME = "pagination";
|
|
39
|
+
var ACTION_SET = /* @__PURE__ */ new Set(["prev", "next", "dismiss"]);
|
|
40
|
+
function paginationCustomId(action, sessionId) {
|
|
41
|
+
return `stambha:${PAGINATION_SIGNAL_NAME}:${action}:${sessionId}`;
|
|
42
|
+
}
|
|
43
|
+
function paginationSuffix(action, sessionId) {
|
|
44
|
+
return `${action}:${sessionId}`;
|
|
45
|
+
}
|
|
46
|
+
function parsePaginationSuffix(suffix) {
|
|
47
|
+
const sep = suffix.indexOf(":");
|
|
48
|
+
if (sep <= 0) return null;
|
|
49
|
+
const action = suffix.slice(0, sep);
|
|
50
|
+
const sessionId = suffix.slice(sep + 1);
|
|
51
|
+
if (!sessionId || !ACTION_SET.has(action)) return null;
|
|
52
|
+
return { action, sessionId };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/components.ts
|
|
56
|
+
var BUTTON = 2;
|
|
57
|
+
var ACTION_ROW = 1;
|
|
58
|
+
var STYLE_SECONDARY = 2;
|
|
59
|
+
var STYLE_DANGER = 4;
|
|
60
|
+
function buildPagePayload(page, options) {
|
|
61
|
+
const { sessionId, index, pageCount, labels, wrap } = options;
|
|
62
|
+
const atStart = index <= 0;
|
|
63
|
+
const atEnd = index >= pageCount - 1;
|
|
64
|
+
const prevDisabled = !wrap && atStart;
|
|
65
|
+
const nextDisabled = !wrap && atEnd;
|
|
66
|
+
const prev = {
|
|
67
|
+
type: BUTTON,
|
|
68
|
+
style: STYLE_SECONDARY,
|
|
69
|
+
label: labels.prev,
|
|
70
|
+
custom_id: paginationCustomId("prev", sessionId)
|
|
71
|
+
};
|
|
72
|
+
if (prevDisabled) prev.disabled = true;
|
|
73
|
+
const next = {
|
|
74
|
+
type: BUTTON,
|
|
75
|
+
style: STYLE_SECONDARY,
|
|
76
|
+
label: labels.next,
|
|
77
|
+
custom_id: paginationCustomId("next", sessionId)
|
|
78
|
+
};
|
|
79
|
+
if (nextDisabled) next.disabled = true;
|
|
80
|
+
const dismiss = {
|
|
81
|
+
type: BUTTON,
|
|
82
|
+
style: STYLE_DANGER,
|
|
83
|
+
label: labels.dismiss,
|
|
84
|
+
custom_id: paginationCustomId("dismiss", sessionId)
|
|
85
|
+
};
|
|
86
|
+
const row = {
|
|
87
|
+
type: ACTION_ROW,
|
|
88
|
+
components: [prev, next, dismiss]
|
|
89
|
+
};
|
|
90
|
+
const payload = {
|
|
91
|
+
components: [row]
|
|
92
|
+
};
|
|
93
|
+
if (page.content !== void 0) payload.content = page.content;
|
|
94
|
+
if (page.embeds !== void 0) payload.embeds = page.embeds;
|
|
95
|
+
return payload;
|
|
96
|
+
}
|
|
97
|
+
function buildDismissPayload(page) {
|
|
98
|
+
const payload = {
|
|
99
|
+
components: []
|
|
100
|
+
};
|
|
101
|
+
if (page.content !== void 0) payload.content = page.content;
|
|
102
|
+
if (page.embeds !== void 0) payload.embeds = page.embeds;
|
|
103
|
+
return payload;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/pageAt.ts
|
|
107
|
+
function pageAt(pages, index) {
|
|
108
|
+
return pages[index] ?? pages[0] ?? {};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/session.ts
|
|
112
|
+
var import_node_crypto = require("crypto");
|
|
113
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
114
|
+
function createSessionId() {
|
|
115
|
+
return (0, import_node_crypto.randomBytes)(6).toString("base64url");
|
|
116
|
+
}
|
|
117
|
+
function setSession(id, session) {
|
|
118
|
+
sessions.set(id, session);
|
|
119
|
+
}
|
|
120
|
+
function getSession(id) {
|
|
121
|
+
const session = sessions.get(id);
|
|
122
|
+
if (!session) return void 0;
|
|
123
|
+
if (Date.now() > session.expiresAt) {
|
|
124
|
+
sessions.delete(id);
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
return session;
|
|
128
|
+
}
|
|
129
|
+
function deleteSession(id) {
|
|
130
|
+
sessions.delete(id);
|
|
131
|
+
}
|
|
132
|
+
function touchSession(id) {
|
|
133
|
+
const session = getSession(id);
|
|
134
|
+
if (!session) return void 0;
|
|
135
|
+
session.expiresAt = Date.now() + session.timeoutMs;
|
|
136
|
+
return session;
|
|
137
|
+
}
|
|
138
|
+
function clearSessions() {
|
|
139
|
+
sessions.clear();
|
|
140
|
+
}
|
|
141
|
+
function sessionCount() {
|
|
142
|
+
return sessions.size;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/createPaginator.ts
|
|
146
|
+
var DEFAULT_TIMEOUT_MS = 5 * 6e4;
|
|
147
|
+
var DEFAULT_LABELS = {
|
|
148
|
+
prev: "Prev",
|
|
149
|
+
next: "Next",
|
|
150
|
+
dismiss: "Dismiss"
|
|
151
|
+
};
|
|
152
|
+
function resolveLabels(labels) {
|
|
153
|
+
return {
|
|
154
|
+
prev: labels?.prev ?? DEFAULT_LABELS.prev,
|
|
155
|
+
next: labels?.next ?? DEFAULT_LABELS.next,
|
|
156
|
+
dismiss: labels?.dismiss ?? DEFAULT_LABELS.dismiss
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async function resolvePages(pages) {
|
|
160
|
+
const resolved = typeof pages === "function" ? await pages() : pages;
|
|
161
|
+
const list = [...resolved];
|
|
162
|
+
if (list.length === 0) {
|
|
163
|
+
throw new Error("@stambha/pagination: pages must be a non-empty array");
|
|
164
|
+
}
|
|
165
|
+
return list;
|
|
166
|
+
}
|
|
167
|
+
async function createPaginator(options) {
|
|
168
|
+
const pages = await resolvePages(options.pages);
|
|
169
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
170
|
+
const wrap = options.wrap ?? false;
|
|
171
|
+
const labels = resolveLabels(options.labels);
|
|
172
|
+
const startAt = options.startAt ?? 0;
|
|
173
|
+
const index = Math.min(Math.max(0, startAt), pages.length - 1);
|
|
174
|
+
const sessionId = createSessionId();
|
|
175
|
+
const session = {
|
|
176
|
+
pages,
|
|
177
|
+
index,
|
|
178
|
+
expiresAt: Date.now() + timeoutMs,
|
|
179
|
+
wrap,
|
|
180
|
+
labels,
|
|
181
|
+
timeoutMs,
|
|
182
|
+
...options.userId !== void 0 ? { userId: options.userId } : {}
|
|
183
|
+
};
|
|
184
|
+
setSession(sessionId, session);
|
|
185
|
+
return {
|
|
186
|
+
sessionId,
|
|
187
|
+
get index() {
|
|
188
|
+
return session.index;
|
|
189
|
+
},
|
|
190
|
+
get pageCount() {
|
|
191
|
+
return pages.length;
|
|
192
|
+
},
|
|
193
|
+
message() {
|
|
194
|
+
const page = pageAt(pages, session.index);
|
|
195
|
+
return buildPagePayload(page, {
|
|
196
|
+
sessionId,
|
|
197
|
+
index: session.index,
|
|
198
|
+
pageCount: pages.length,
|
|
199
|
+
labels,
|
|
200
|
+
wrap
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/PaginationSignal.ts
|
|
207
|
+
var import_core2 = require("@stambha/core");
|
|
208
|
+
|
|
209
|
+
// src/respond.ts
|
|
210
|
+
var import_core = require("@stambha/core");
|
|
211
|
+
async function respondWithPage(ctx, restPort, payload) {
|
|
212
|
+
const raw = ctx.raw;
|
|
213
|
+
const id = raw?.id ?? null;
|
|
214
|
+
const token = raw?.token ?? null;
|
|
215
|
+
if (restPort && id && token) {
|
|
216
|
+
await restPort.request({
|
|
217
|
+
method: "POST",
|
|
218
|
+
route: `/interactions/${id}/${token}/callback`,
|
|
219
|
+
body: {
|
|
220
|
+
type: 7,
|
|
221
|
+
data: (0, import_core.normalizeReplyData)(payload)
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
await ctx.reply(payload);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/PaginationSignal.ts
|
|
230
|
+
var PaginationSignal = class extends import_core2.Signal {
|
|
231
|
+
constructor(registry) {
|
|
232
|
+
super(registry, {
|
|
233
|
+
name: PAGINATION_SIGNAL_NAME,
|
|
234
|
+
types: ["button"]
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async run(ctx) {
|
|
238
|
+
const parsed = import_core2.Signal.parseCustomId(ctx.customId);
|
|
239
|
+
if (!parsed || parsed.name !== PAGINATION_SIGNAL_NAME) return;
|
|
240
|
+
const parts = parsePaginationSuffix(parsed.suffix);
|
|
241
|
+
if (!parts) {
|
|
242
|
+
await ctx.replyEphemeral("This pagination control is invalid.");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const session = getSession(parts.sessionId);
|
|
246
|
+
if (!session) {
|
|
247
|
+
await ctx.replyEphemeral("This paginator has expired. Run the command again.");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (session.userId !== void 0 && session.userId !== ctx.userId) {
|
|
251
|
+
await ctx.replyEphemeral("Only the user who opened this paginator can use it.");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const restPort = this.client.restPort;
|
|
255
|
+
const pageCount = session.pages.length;
|
|
256
|
+
if (parts.action === "dismiss") {
|
|
257
|
+
const page2 = pageAt(session.pages, session.index);
|
|
258
|
+
deleteSession(parts.sessionId);
|
|
259
|
+
await respondWithPage(ctx, restPort, buildDismissPayload(page2));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
let nextIndex = session.index;
|
|
263
|
+
if (parts.action === "prev") {
|
|
264
|
+
if (session.wrap) {
|
|
265
|
+
nextIndex = (session.index - 1 + pageCount) % pageCount;
|
|
266
|
+
} else if (session.index > 0) {
|
|
267
|
+
nextIndex = session.index - 1;
|
|
268
|
+
}
|
|
269
|
+
} else if (parts.action === "next") {
|
|
270
|
+
if (session.wrap) {
|
|
271
|
+
nextIndex = (session.index + 1) % pageCount;
|
|
272
|
+
} else if (session.index < pageCount - 1) {
|
|
273
|
+
nextIndex = session.index + 1;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
session.index = nextIndex;
|
|
277
|
+
touchSession(parts.sessionId);
|
|
278
|
+
const page = pageAt(session.pages, session.index);
|
|
279
|
+
await respondWithPage(
|
|
280
|
+
ctx,
|
|
281
|
+
restPort,
|
|
282
|
+
buildPagePayload(page, {
|
|
283
|
+
sessionId: parts.sessionId,
|
|
284
|
+
index: session.index,
|
|
285
|
+
pageCount,
|
|
286
|
+
labels: session.labels,
|
|
287
|
+
wrap: session.wrap
|
|
288
|
+
})
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
293
|
+
0 && (module.exports = {
|
|
294
|
+
PAGINATION_SIGNAL_NAME,
|
|
295
|
+
PaginationSignal,
|
|
296
|
+
buildDismissPayload,
|
|
297
|
+
buildPagePayload,
|
|
298
|
+
clearSessions,
|
|
299
|
+
createPaginator,
|
|
300
|
+
getSession,
|
|
301
|
+
paginationCustomId,
|
|
302
|
+
paginationSuffix,
|
|
303
|
+
parsePaginationSuffix,
|
|
304
|
+
sessionCount
|
|
305
|
+
});
|
|
306
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/ids.ts","../src/components.ts","../src/pageAt.ts","../src/session.ts","../src/createPaginator.ts","../src/PaginationSignal.ts","../src/respond.ts"],"sourcesContent":["export { buildDismissPayload, buildPagePayload } from \"./components.js\";\nexport { createPaginator } from \"./createPaginator.js\";\nexport {\n PAGINATION_SIGNAL_NAME,\n paginationCustomId,\n paginationSuffix,\n parsePaginationSuffix,\n} from \"./ids.js\";\nexport { PaginationSignal } from \"./PaginationSignal.js\";\nexport { clearSessions, getSession, sessionCount } from \"./session.js\";\nexport type {\n Page,\n PaginationAction,\n PaginationSession,\n Paginator,\n PaginatorLabels,\n PaginatorOptions,\n ResolvedPaginatorLabels,\n} from \"./types.js\";\n","import type { PaginationAction } from \"./types.js\";\n\nexport const PAGINATION_SIGNAL_NAME = \"pagination\";\n\nconst ACTION_SET = new Set<PaginationAction>([\"prev\", \"next\", \"dismiss\"]);\n\n/** Build a routable custom id: `stambha:pagination:{action}:{sessionId}`. */\nexport function paginationCustomId(action: PaginationAction, sessionId: string): string {\n return `stambha:${PAGINATION_SIGNAL_NAME}:${action}:${sessionId}`;\n}\n\n/** Suffix passed to {@link Signal.customId} / returned by {@link Signal.parseCustomId}. */\nexport function paginationSuffix(action: PaginationAction, sessionId: string): string {\n return `${action}:${sessionId}`;\n}\n\nexport function parsePaginationSuffix(\n suffix: string,\n): { action: PaginationAction; sessionId: string } | null {\n const sep = suffix.indexOf(\":\");\n if (sep <= 0) return null;\n const action = suffix.slice(0, sep);\n const sessionId = suffix.slice(sep + 1);\n if (!sessionId || !ACTION_SET.has(action as PaginationAction)) return null;\n return { action: action as PaginationAction, sessionId };\n}\n","import type { ReplyPayload } from \"@stambha/core\";\nimport { paginationCustomId } from \"./ids.js\";\nimport type { Page, ResolvedPaginatorLabels } from \"./types.js\";\n\nconst BUTTON = 2;\nconst ACTION_ROW = 1;\nconst STYLE_SECONDARY = 2;\nconst STYLE_DANGER = 4;\n\ninterface ButtonComponent {\n type: number;\n style: number;\n label: string;\n custom_id: string;\n disabled?: boolean;\n}\n\ninterface ActionRow {\n type: number;\n components: ButtonComponent[];\n}\n\nexport function buildPagePayload(\n page: Page,\n options: {\n sessionId: string;\n index: number;\n pageCount: number;\n labels: ResolvedPaginatorLabels;\n wrap: boolean;\n },\n): ReplyPayload {\n const { sessionId, index, pageCount, labels, wrap } = options;\n const atStart = index <= 0;\n const atEnd = index >= pageCount - 1;\n\n const prevDisabled = !wrap && atStart;\n const nextDisabled = !wrap && atEnd;\n\n const prev: ButtonComponent = {\n type: BUTTON,\n style: STYLE_SECONDARY,\n label: labels.prev,\n custom_id: paginationCustomId(\"prev\", sessionId),\n };\n if (prevDisabled) prev.disabled = true;\n\n const next: ButtonComponent = {\n type: BUTTON,\n style: STYLE_SECONDARY,\n label: labels.next,\n custom_id: paginationCustomId(\"next\", sessionId),\n };\n if (nextDisabled) next.disabled = true;\n\n const dismiss: ButtonComponent = {\n type: BUTTON,\n style: STYLE_DANGER,\n label: labels.dismiss,\n custom_id: paginationCustomId(\"dismiss\", sessionId),\n };\n\n const row: ActionRow = {\n type: ACTION_ROW,\n components: [prev, next, dismiss],\n };\n\n const payload: ReplyPayload = {\n components: [row],\n };\n if (page.content !== undefined) payload.content = page.content;\n if (page.embeds !== undefined) payload.embeds = page.embeds;\n return payload;\n}\n\n/** Same page content with controls removed (dismiss). */\nexport function buildDismissPayload(page: Page): ReplyPayload {\n const payload: ReplyPayload = {\n components: [],\n };\n if (page.content !== undefined) payload.content = page.content;\n if (page.embeds !== undefined) payload.embeds = page.embeds;\n return payload;\n}\n","import type { Page } from \"./types.js\";\n\n/** Sessions always have ≥1 page; return a safe page for the index. */\nexport function pageAt(pages: readonly Page[], index: number): Page {\n return pages[index] ?? pages[0] ?? {};\n}\n","import { randomBytes } from \"node:crypto\";\nimport type { PaginationSession } from \"./types.js\";\n\nconst sessions = new Map<string, PaginationSession>();\n\n/** Create a short opaque session id (fits Discord custom_id limits). */\nexport function createSessionId(): string {\n return randomBytes(6).toString(\"base64url\");\n}\n\nexport function setSession(id: string, session: PaginationSession): void {\n sessions.set(id, session);\n}\n\nexport function getSession(id: string): PaginationSession | undefined {\n const session = sessions.get(id);\n if (!session) return undefined;\n if (Date.now() > session.expiresAt) {\n sessions.delete(id);\n return undefined;\n }\n return session;\n}\n\nexport function deleteSession(id: string): void {\n sessions.delete(id);\n}\n\n/** Touch TTL after a successful interaction. */\nexport function touchSession(id: string): PaginationSession | undefined {\n const session = getSession(id);\n if (!session) return undefined;\n session.expiresAt = Date.now() + session.timeoutMs;\n return session;\n}\n\n/** Test helper — clear all sessions. */\nexport function clearSessions(): void {\n sessions.clear();\n}\n\n/** Test helper — current session count (includes unexpired only when read via get). */\nexport function sessionCount(): number {\n return sessions.size;\n}\n","import type { ReplyPayload } from \"@stambha/core\";\nimport { buildPagePayload } from \"./components.js\";\nimport { pageAt } from \"./pageAt.js\";\nimport { createSessionId, setSession } from \"./session.js\";\nimport type { Page, Paginator, PaginatorOptions, ResolvedPaginatorLabels } from \"./types.js\";\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60_000;\n\nconst DEFAULT_LABELS: ResolvedPaginatorLabels = {\n prev: \"Prev\",\n next: \"Next\",\n dismiss: \"Dismiss\",\n};\n\nfunction resolveLabels(labels?: PaginatorOptions[\"labels\"]): ResolvedPaginatorLabels {\n return {\n prev: labels?.prev ?? DEFAULT_LABELS.prev,\n next: labels?.next ?? DEFAULT_LABELS.next,\n dismiss: labels?.dismiss ?? DEFAULT_LABELS.dismiss,\n };\n}\n\nasync function resolvePages(pages: PaginatorOptions[\"pages\"]): Promise<Page[]> {\n const resolved = typeof pages === \"function\" ? await pages() : pages;\n const list = [...resolved];\n if (list.length === 0) {\n throw new Error(\"@stambha/pagination: pages must be a non-empty array\");\n }\n return list;\n}\n\n/**\n * Create an in-memory paginator session and return a helper that builds the\n * initial message payload (page content + prev/next/dismiss buttons).\n */\nexport async function createPaginator(options: PaginatorOptions): Promise<Paginator> {\n const pages = await resolvePages(options.pages);\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const wrap = options.wrap ?? false;\n const labels = resolveLabels(options.labels);\n const startAt = options.startAt ?? 0;\n const index = Math.min(Math.max(0, startAt), pages.length - 1);\n const sessionId = createSessionId();\n\n const session = {\n pages,\n index,\n expiresAt: Date.now() + timeoutMs,\n wrap,\n labels,\n timeoutMs,\n ...(options.userId !== undefined ? { userId: options.userId } : {}),\n };\n setSession(sessionId, session);\n\n return {\n sessionId,\n get index() {\n return session.index;\n },\n get pageCount() {\n return pages.length;\n },\n message(): ReplyPayload {\n const page = pageAt(pages, session.index);\n return buildPagePayload(page, {\n sessionId,\n index: session.index,\n pageCount: pages.length,\n labels,\n wrap,\n });\n },\n };\n}\n","import { type Registry, Signal, type SignalContext } from \"@stambha/core\";\nimport { buildDismissPayload, buildPagePayload } from \"./components.js\";\nimport { PAGINATION_SIGNAL_NAME, parsePaginationSuffix } from \"./ids.js\";\nimport { pageAt } from \"./pageAt.js\";\nimport { respondWithPage } from \"./respond.js\";\nimport { deleteSession, getSession, touchSession } from \"./session.js\";\n\n/**\n * Signal piece that handles `stambha:pagination:{action}:{sessionId}` button clicks.\n * Register under `src/signals/` (or construct and `registries.signals.register`).\n */\nexport class PaginationSignal extends Signal {\n constructor(registry: Registry<Signal>) {\n super(registry, {\n name: PAGINATION_SIGNAL_NAME,\n types: [\"button\"],\n });\n }\n\n async run(ctx: SignalContext): Promise<void> {\n const parsed = Signal.parseCustomId(ctx.customId);\n if (!parsed || parsed.name !== PAGINATION_SIGNAL_NAME) return;\n\n const parts = parsePaginationSuffix(parsed.suffix);\n if (!parts) {\n await ctx.replyEphemeral(\"This pagination control is invalid.\");\n return;\n }\n\n const session = getSession(parts.sessionId);\n if (!session) {\n await ctx.replyEphemeral(\"This paginator has expired. Run the command again.\");\n return;\n }\n\n if (session.userId !== undefined && session.userId !== ctx.userId) {\n await ctx.replyEphemeral(\"Only the user who opened this paginator can use it.\");\n return;\n }\n\n const restPort = this.client.restPort;\n const pageCount = session.pages.length;\n\n if (parts.action === \"dismiss\") {\n const page = pageAt(session.pages, session.index);\n deleteSession(parts.sessionId);\n await respondWithPage(ctx, restPort, buildDismissPayload(page));\n return;\n }\n\n let nextIndex = session.index;\n if (parts.action === \"prev\") {\n if (session.wrap) {\n nextIndex = (session.index - 1 + pageCount) % pageCount;\n } else if (session.index > 0) {\n nextIndex = session.index - 1;\n }\n } else if (parts.action === \"next\") {\n if (session.wrap) {\n nextIndex = (session.index + 1) % pageCount;\n } else if (session.index < pageCount - 1) {\n nextIndex = session.index + 1;\n }\n }\n\n session.index = nextIndex;\n touchSession(parts.sessionId);\n\n const page = pageAt(session.pages, session.index);\n await respondWithPage(\n ctx,\n restPort,\n buildPagePayload(page, {\n sessionId: parts.sessionId,\n index: session.index,\n pageCount,\n labels: session.labels,\n wrap: session.wrap,\n }),\n );\n }\n}\n","import {\n normalizeReplyData,\n type ReplyPayload,\n type RestPort,\n type SignalContext,\n} from \"@stambha/core\";\n\ninterface InteractionRaw {\n id?: string | null;\n token?: string | null;\n}\n\n/**\n * Prefer Discord interaction callback type 7 (UPDATE_MESSAGE) via {@link RestPort}.\n * Falls back to {@link SignalContext.reply} (type 4) when REST is unavailable.\n */\nexport async function respondWithPage(\n ctx: SignalContext,\n restPort: RestPort | null,\n payload: ReplyPayload,\n): Promise<void> {\n const raw = ctx.raw as InteractionRaw | null | undefined;\n const id = raw?.id ?? null;\n const token = raw?.token ?? null;\n\n if (restPort && id && token) {\n await restPort.request({\n method: \"POST\",\n route: `/interactions/${id}/${token}/callback`,\n body: {\n type: 7,\n data: normalizeReplyData(payload),\n },\n });\n return;\n }\n\n await ctx.reply(payload);\n}\n"],"mappings":"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","names":["import_core","page"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ReplyPayload, Signal, Registry, SignalContext } from '@stambha/core';
|
|
2
|
+
|
|
3
|
+
/** One page of content shown by the paginator (components are owned by the package). */
|
|
4
|
+
interface Page {
|
|
5
|
+
content?: string;
|
|
6
|
+
embeds?: readonly unknown[];
|
|
7
|
+
}
|
|
8
|
+
type PaginationAction = "prev" | "next" | "dismiss";
|
|
9
|
+
interface PaginatorLabels {
|
|
10
|
+
prev?: string;
|
|
11
|
+
next?: string;
|
|
12
|
+
dismiss?: string;
|
|
13
|
+
}
|
|
14
|
+
interface ResolvedPaginatorLabels {
|
|
15
|
+
prev: string;
|
|
16
|
+
next: string;
|
|
17
|
+
dismiss: string;
|
|
18
|
+
}
|
|
19
|
+
interface PaginatorOptions {
|
|
20
|
+
/** Static pages or a resolver invoked once when the paginator is created. */
|
|
21
|
+
pages: readonly Page[] | (() => readonly Page[] | Promise<readonly Page[]>);
|
|
22
|
+
/** When set, only this user may click the buttons. */
|
|
23
|
+
userId?: string;
|
|
24
|
+
/** Session TTL in milliseconds (default 5 minutes). */
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
labels?: PaginatorLabels;
|
|
27
|
+
/** Zero-based start index (default 0). */
|
|
28
|
+
startAt?: number;
|
|
29
|
+
/**
|
|
30
|
+
* When true, prev/next wrap around the ends.
|
|
31
|
+
* When false (default), buttons at the ends are disabled.
|
|
32
|
+
*/
|
|
33
|
+
wrap?: boolean;
|
|
34
|
+
}
|
|
35
|
+
interface Paginator {
|
|
36
|
+
readonly sessionId: string;
|
|
37
|
+
/** Current zero-based page index. */
|
|
38
|
+
readonly index: number;
|
|
39
|
+
/** Total page count. */
|
|
40
|
+
readonly pageCount: number;
|
|
41
|
+
/** Build a {@link ReplyPayload} for the current page (content/embeds + buttons). */
|
|
42
|
+
message(): ReplyPayload;
|
|
43
|
+
}
|
|
44
|
+
interface PaginationSession {
|
|
45
|
+
pages: Page[];
|
|
46
|
+
index: number;
|
|
47
|
+
userId?: string;
|
|
48
|
+
expiresAt: number;
|
|
49
|
+
wrap: boolean;
|
|
50
|
+
labels: ResolvedPaginatorLabels;
|
|
51
|
+
timeoutMs: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
declare function buildPagePayload(page: Page, options: {
|
|
55
|
+
sessionId: string;
|
|
56
|
+
index: number;
|
|
57
|
+
pageCount: number;
|
|
58
|
+
labels: ResolvedPaginatorLabels;
|
|
59
|
+
wrap: boolean;
|
|
60
|
+
}): ReplyPayload;
|
|
61
|
+
/** Same page content with controls removed (dismiss). */
|
|
62
|
+
declare function buildDismissPayload(page: Page): ReplyPayload;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create an in-memory paginator session and return a helper that builds the
|
|
66
|
+
* initial message payload (page content + prev/next/dismiss buttons).
|
|
67
|
+
*/
|
|
68
|
+
declare function createPaginator(options: PaginatorOptions): Promise<Paginator>;
|
|
69
|
+
|
|
70
|
+
declare const PAGINATION_SIGNAL_NAME = "pagination";
|
|
71
|
+
/** Build a routable custom id: `stambha:pagination:{action}:{sessionId}`. */
|
|
72
|
+
declare function paginationCustomId(action: PaginationAction, sessionId: string): string;
|
|
73
|
+
/** Suffix passed to {@link Signal.customId} / returned by {@link Signal.parseCustomId}. */
|
|
74
|
+
declare function paginationSuffix(action: PaginationAction, sessionId: string): string;
|
|
75
|
+
declare function parsePaginationSuffix(suffix: string): {
|
|
76
|
+
action: PaginationAction;
|
|
77
|
+
sessionId: string;
|
|
78
|
+
} | null;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Signal piece that handles `stambha:pagination:{action}:{sessionId}` button clicks.
|
|
82
|
+
* Register under `src/signals/` (or construct and `registries.signals.register`).
|
|
83
|
+
*/
|
|
84
|
+
declare class PaginationSignal extends Signal {
|
|
85
|
+
constructor(registry: Registry<Signal>);
|
|
86
|
+
run(ctx: SignalContext): Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
declare function getSession(id: string): PaginationSession | undefined;
|
|
90
|
+
/** Test helper — clear all sessions. */
|
|
91
|
+
declare function clearSessions(): void;
|
|
92
|
+
/** Test helper — current session count (includes unexpired only when read via get). */
|
|
93
|
+
declare function sessionCount(): number;
|
|
94
|
+
|
|
95
|
+
export { PAGINATION_SIGNAL_NAME, type Page, type PaginationAction, type PaginationSession, PaginationSignal, type Paginator, type PaginatorLabels, type PaginatorOptions, type ResolvedPaginatorLabels, buildDismissPayload, buildPagePayload, clearSessions, createPaginator, getSession, paginationCustomId, paginationSuffix, parsePaginationSuffix, sessionCount };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ReplyPayload, Signal, Registry, SignalContext } from '@stambha/core';
|
|
2
|
+
|
|
3
|
+
/** One page of content shown by the paginator (components are owned by the package). */
|
|
4
|
+
interface Page {
|
|
5
|
+
content?: string;
|
|
6
|
+
embeds?: readonly unknown[];
|
|
7
|
+
}
|
|
8
|
+
type PaginationAction = "prev" | "next" | "dismiss";
|
|
9
|
+
interface PaginatorLabels {
|
|
10
|
+
prev?: string;
|
|
11
|
+
next?: string;
|
|
12
|
+
dismiss?: string;
|
|
13
|
+
}
|
|
14
|
+
interface ResolvedPaginatorLabels {
|
|
15
|
+
prev: string;
|
|
16
|
+
next: string;
|
|
17
|
+
dismiss: string;
|
|
18
|
+
}
|
|
19
|
+
interface PaginatorOptions {
|
|
20
|
+
/** Static pages or a resolver invoked once when the paginator is created. */
|
|
21
|
+
pages: readonly Page[] | (() => readonly Page[] | Promise<readonly Page[]>);
|
|
22
|
+
/** When set, only this user may click the buttons. */
|
|
23
|
+
userId?: string;
|
|
24
|
+
/** Session TTL in milliseconds (default 5 minutes). */
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
labels?: PaginatorLabels;
|
|
27
|
+
/** Zero-based start index (default 0). */
|
|
28
|
+
startAt?: number;
|
|
29
|
+
/**
|
|
30
|
+
* When true, prev/next wrap around the ends.
|
|
31
|
+
* When false (default), buttons at the ends are disabled.
|
|
32
|
+
*/
|
|
33
|
+
wrap?: boolean;
|
|
34
|
+
}
|
|
35
|
+
interface Paginator {
|
|
36
|
+
readonly sessionId: string;
|
|
37
|
+
/** Current zero-based page index. */
|
|
38
|
+
readonly index: number;
|
|
39
|
+
/** Total page count. */
|
|
40
|
+
readonly pageCount: number;
|
|
41
|
+
/** Build a {@link ReplyPayload} for the current page (content/embeds + buttons). */
|
|
42
|
+
message(): ReplyPayload;
|
|
43
|
+
}
|
|
44
|
+
interface PaginationSession {
|
|
45
|
+
pages: Page[];
|
|
46
|
+
index: number;
|
|
47
|
+
userId?: string;
|
|
48
|
+
expiresAt: number;
|
|
49
|
+
wrap: boolean;
|
|
50
|
+
labels: ResolvedPaginatorLabels;
|
|
51
|
+
timeoutMs: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
declare function buildPagePayload(page: Page, options: {
|
|
55
|
+
sessionId: string;
|
|
56
|
+
index: number;
|
|
57
|
+
pageCount: number;
|
|
58
|
+
labels: ResolvedPaginatorLabels;
|
|
59
|
+
wrap: boolean;
|
|
60
|
+
}): ReplyPayload;
|
|
61
|
+
/** Same page content with controls removed (dismiss). */
|
|
62
|
+
declare function buildDismissPayload(page: Page): ReplyPayload;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create an in-memory paginator session and return a helper that builds the
|
|
66
|
+
* initial message payload (page content + prev/next/dismiss buttons).
|
|
67
|
+
*/
|
|
68
|
+
declare function createPaginator(options: PaginatorOptions): Promise<Paginator>;
|
|
69
|
+
|
|
70
|
+
declare const PAGINATION_SIGNAL_NAME = "pagination";
|
|
71
|
+
/** Build a routable custom id: `stambha:pagination:{action}:{sessionId}`. */
|
|
72
|
+
declare function paginationCustomId(action: PaginationAction, sessionId: string): string;
|
|
73
|
+
/** Suffix passed to {@link Signal.customId} / returned by {@link Signal.parseCustomId}. */
|
|
74
|
+
declare function paginationSuffix(action: PaginationAction, sessionId: string): string;
|
|
75
|
+
declare function parsePaginationSuffix(suffix: string): {
|
|
76
|
+
action: PaginationAction;
|
|
77
|
+
sessionId: string;
|
|
78
|
+
} | null;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Signal piece that handles `stambha:pagination:{action}:{sessionId}` button clicks.
|
|
82
|
+
* Register under `src/signals/` (or construct and `registries.signals.register`).
|
|
83
|
+
*/
|
|
84
|
+
declare class PaginationSignal extends Signal {
|
|
85
|
+
constructor(registry: Registry<Signal>);
|
|
86
|
+
run(ctx: SignalContext): Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
declare function getSession(id: string): PaginationSession | undefined;
|
|
90
|
+
/** Test helper — clear all sessions. */
|
|
91
|
+
declare function clearSessions(): void;
|
|
92
|
+
/** Test helper — current session count (includes unexpired only when read via get). */
|
|
93
|
+
declare function sessionCount(): number;
|
|
94
|
+
|
|
95
|
+
export { PAGINATION_SIGNAL_NAME, type Page, type PaginationAction, type PaginationSession, PaginationSignal, type Paginator, type PaginatorLabels, type PaginatorOptions, type ResolvedPaginatorLabels, buildDismissPayload, buildPagePayload, clearSessions, createPaginator, getSession, paginationCustomId, paginationSuffix, parsePaginationSuffix, sessionCount };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// src/ids.ts
|
|
2
|
+
var PAGINATION_SIGNAL_NAME = "pagination";
|
|
3
|
+
var ACTION_SET = /* @__PURE__ */ new Set(["prev", "next", "dismiss"]);
|
|
4
|
+
function paginationCustomId(action, sessionId) {
|
|
5
|
+
return `stambha:${PAGINATION_SIGNAL_NAME}:${action}:${sessionId}`;
|
|
6
|
+
}
|
|
7
|
+
function paginationSuffix(action, sessionId) {
|
|
8
|
+
return `${action}:${sessionId}`;
|
|
9
|
+
}
|
|
10
|
+
function parsePaginationSuffix(suffix) {
|
|
11
|
+
const sep = suffix.indexOf(":");
|
|
12
|
+
if (sep <= 0) return null;
|
|
13
|
+
const action = suffix.slice(0, sep);
|
|
14
|
+
const sessionId = suffix.slice(sep + 1);
|
|
15
|
+
if (!sessionId || !ACTION_SET.has(action)) return null;
|
|
16
|
+
return { action, sessionId };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/components.ts
|
|
20
|
+
var BUTTON = 2;
|
|
21
|
+
var ACTION_ROW = 1;
|
|
22
|
+
var STYLE_SECONDARY = 2;
|
|
23
|
+
var STYLE_DANGER = 4;
|
|
24
|
+
function buildPagePayload(page, options) {
|
|
25
|
+
const { sessionId, index, pageCount, labels, wrap } = options;
|
|
26
|
+
const atStart = index <= 0;
|
|
27
|
+
const atEnd = index >= pageCount - 1;
|
|
28
|
+
const prevDisabled = !wrap && atStart;
|
|
29
|
+
const nextDisabled = !wrap && atEnd;
|
|
30
|
+
const prev = {
|
|
31
|
+
type: BUTTON,
|
|
32
|
+
style: STYLE_SECONDARY,
|
|
33
|
+
label: labels.prev,
|
|
34
|
+
custom_id: paginationCustomId("prev", sessionId)
|
|
35
|
+
};
|
|
36
|
+
if (prevDisabled) prev.disabled = true;
|
|
37
|
+
const next = {
|
|
38
|
+
type: BUTTON,
|
|
39
|
+
style: STYLE_SECONDARY,
|
|
40
|
+
label: labels.next,
|
|
41
|
+
custom_id: paginationCustomId("next", sessionId)
|
|
42
|
+
};
|
|
43
|
+
if (nextDisabled) next.disabled = true;
|
|
44
|
+
const dismiss = {
|
|
45
|
+
type: BUTTON,
|
|
46
|
+
style: STYLE_DANGER,
|
|
47
|
+
label: labels.dismiss,
|
|
48
|
+
custom_id: paginationCustomId("dismiss", sessionId)
|
|
49
|
+
};
|
|
50
|
+
const row = {
|
|
51
|
+
type: ACTION_ROW,
|
|
52
|
+
components: [prev, next, dismiss]
|
|
53
|
+
};
|
|
54
|
+
const payload = {
|
|
55
|
+
components: [row]
|
|
56
|
+
};
|
|
57
|
+
if (page.content !== void 0) payload.content = page.content;
|
|
58
|
+
if (page.embeds !== void 0) payload.embeds = page.embeds;
|
|
59
|
+
return payload;
|
|
60
|
+
}
|
|
61
|
+
function buildDismissPayload(page) {
|
|
62
|
+
const payload = {
|
|
63
|
+
components: []
|
|
64
|
+
};
|
|
65
|
+
if (page.content !== void 0) payload.content = page.content;
|
|
66
|
+
if (page.embeds !== void 0) payload.embeds = page.embeds;
|
|
67
|
+
return payload;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/pageAt.ts
|
|
71
|
+
function pageAt(pages, index) {
|
|
72
|
+
return pages[index] ?? pages[0] ?? {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/session.ts
|
|
76
|
+
import { randomBytes } from "crypto";
|
|
77
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
78
|
+
function createSessionId() {
|
|
79
|
+
return randomBytes(6).toString("base64url");
|
|
80
|
+
}
|
|
81
|
+
function setSession(id, session) {
|
|
82
|
+
sessions.set(id, session);
|
|
83
|
+
}
|
|
84
|
+
function getSession(id) {
|
|
85
|
+
const session = sessions.get(id);
|
|
86
|
+
if (!session) return void 0;
|
|
87
|
+
if (Date.now() > session.expiresAt) {
|
|
88
|
+
sessions.delete(id);
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
return session;
|
|
92
|
+
}
|
|
93
|
+
function deleteSession(id) {
|
|
94
|
+
sessions.delete(id);
|
|
95
|
+
}
|
|
96
|
+
function touchSession(id) {
|
|
97
|
+
const session = getSession(id);
|
|
98
|
+
if (!session) return void 0;
|
|
99
|
+
session.expiresAt = Date.now() + session.timeoutMs;
|
|
100
|
+
return session;
|
|
101
|
+
}
|
|
102
|
+
function clearSessions() {
|
|
103
|
+
sessions.clear();
|
|
104
|
+
}
|
|
105
|
+
function sessionCount() {
|
|
106
|
+
return sessions.size;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/createPaginator.ts
|
|
110
|
+
var DEFAULT_TIMEOUT_MS = 5 * 6e4;
|
|
111
|
+
var DEFAULT_LABELS = {
|
|
112
|
+
prev: "Prev",
|
|
113
|
+
next: "Next",
|
|
114
|
+
dismiss: "Dismiss"
|
|
115
|
+
};
|
|
116
|
+
function resolveLabels(labels) {
|
|
117
|
+
return {
|
|
118
|
+
prev: labels?.prev ?? DEFAULT_LABELS.prev,
|
|
119
|
+
next: labels?.next ?? DEFAULT_LABELS.next,
|
|
120
|
+
dismiss: labels?.dismiss ?? DEFAULT_LABELS.dismiss
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function resolvePages(pages) {
|
|
124
|
+
const resolved = typeof pages === "function" ? await pages() : pages;
|
|
125
|
+
const list = [...resolved];
|
|
126
|
+
if (list.length === 0) {
|
|
127
|
+
throw new Error("@stambha/pagination: pages must be a non-empty array");
|
|
128
|
+
}
|
|
129
|
+
return list;
|
|
130
|
+
}
|
|
131
|
+
async function createPaginator(options) {
|
|
132
|
+
const pages = await resolvePages(options.pages);
|
|
133
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
134
|
+
const wrap = options.wrap ?? false;
|
|
135
|
+
const labels = resolveLabels(options.labels);
|
|
136
|
+
const startAt = options.startAt ?? 0;
|
|
137
|
+
const index = Math.min(Math.max(0, startAt), pages.length - 1);
|
|
138
|
+
const sessionId = createSessionId();
|
|
139
|
+
const session = {
|
|
140
|
+
pages,
|
|
141
|
+
index,
|
|
142
|
+
expiresAt: Date.now() + timeoutMs,
|
|
143
|
+
wrap,
|
|
144
|
+
labels,
|
|
145
|
+
timeoutMs,
|
|
146
|
+
...options.userId !== void 0 ? { userId: options.userId } : {}
|
|
147
|
+
};
|
|
148
|
+
setSession(sessionId, session);
|
|
149
|
+
return {
|
|
150
|
+
sessionId,
|
|
151
|
+
get index() {
|
|
152
|
+
return session.index;
|
|
153
|
+
},
|
|
154
|
+
get pageCount() {
|
|
155
|
+
return pages.length;
|
|
156
|
+
},
|
|
157
|
+
message() {
|
|
158
|
+
const page = pageAt(pages, session.index);
|
|
159
|
+
return buildPagePayload(page, {
|
|
160
|
+
sessionId,
|
|
161
|
+
index: session.index,
|
|
162
|
+
pageCount: pages.length,
|
|
163
|
+
labels,
|
|
164
|
+
wrap
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/PaginationSignal.ts
|
|
171
|
+
import { Signal } from "@stambha/core";
|
|
172
|
+
|
|
173
|
+
// src/respond.ts
|
|
174
|
+
import {
|
|
175
|
+
normalizeReplyData
|
|
176
|
+
} from "@stambha/core";
|
|
177
|
+
async function respondWithPage(ctx, restPort, payload) {
|
|
178
|
+
const raw = ctx.raw;
|
|
179
|
+
const id = raw?.id ?? null;
|
|
180
|
+
const token = raw?.token ?? null;
|
|
181
|
+
if (restPort && id && token) {
|
|
182
|
+
await restPort.request({
|
|
183
|
+
method: "POST",
|
|
184
|
+
route: `/interactions/${id}/${token}/callback`,
|
|
185
|
+
body: {
|
|
186
|
+
type: 7,
|
|
187
|
+
data: normalizeReplyData(payload)
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
await ctx.reply(payload);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/PaginationSignal.ts
|
|
196
|
+
var PaginationSignal = class extends Signal {
|
|
197
|
+
constructor(registry) {
|
|
198
|
+
super(registry, {
|
|
199
|
+
name: PAGINATION_SIGNAL_NAME,
|
|
200
|
+
types: ["button"]
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
async run(ctx) {
|
|
204
|
+
const parsed = Signal.parseCustomId(ctx.customId);
|
|
205
|
+
if (!parsed || parsed.name !== PAGINATION_SIGNAL_NAME) return;
|
|
206
|
+
const parts = parsePaginationSuffix(parsed.suffix);
|
|
207
|
+
if (!parts) {
|
|
208
|
+
await ctx.replyEphemeral("This pagination control is invalid.");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const session = getSession(parts.sessionId);
|
|
212
|
+
if (!session) {
|
|
213
|
+
await ctx.replyEphemeral("This paginator has expired. Run the command again.");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (session.userId !== void 0 && session.userId !== ctx.userId) {
|
|
217
|
+
await ctx.replyEphemeral("Only the user who opened this paginator can use it.");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const restPort = this.client.restPort;
|
|
221
|
+
const pageCount = session.pages.length;
|
|
222
|
+
if (parts.action === "dismiss") {
|
|
223
|
+
const page2 = pageAt(session.pages, session.index);
|
|
224
|
+
deleteSession(parts.sessionId);
|
|
225
|
+
await respondWithPage(ctx, restPort, buildDismissPayload(page2));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
let nextIndex = session.index;
|
|
229
|
+
if (parts.action === "prev") {
|
|
230
|
+
if (session.wrap) {
|
|
231
|
+
nextIndex = (session.index - 1 + pageCount) % pageCount;
|
|
232
|
+
} else if (session.index > 0) {
|
|
233
|
+
nextIndex = session.index - 1;
|
|
234
|
+
}
|
|
235
|
+
} else if (parts.action === "next") {
|
|
236
|
+
if (session.wrap) {
|
|
237
|
+
nextIndex = (session.index + 1) % pageCount;
|
|
238
|
+
} else if (session.index < pageCount - 1) {
|
|
239
|
+
nextIndex = session.index + 1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
session.index = nextIndex;
|
|
243
|
+
touchSession(parts.sessionId);
|
|
244
|
+
const page = pageAt(session.pages, session.index);
|
|
245
|
+
await respondWithPage(
|
|
246
|
+
ctx,
|
|
247
|
+
restPort,
|
|
248
|
+
buildPagePayload(page, {
|
|
249
|
+
sessionId: parts.sessionId,
|
|
250
|
+
index: session.index,
|
|
251
|
+
pageCount,
|
|
252
|
+
labels: session.labels,
|
|
253
|
+
wrap: session.wrap
|
|
254
|
+
})
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
export {
|
|
259
|
+
PAGINATION_SIGNAL_NAME,
|
|
260
|
+
PaginationSignal,
|
|
261
|
+
buildDismissPayload,
|
|
262
|
+
buildPagePayload,
|
|
263
|
+
clearSessions,
|
|
264
|
+
createPaginator,
|
|
265
|
+
getSession,
|
|
266
|
+
paginationCustomId,
|
|
267
|
+
paginationSuffix,
|
|
268
|
+
parsePaginationSuffix,
|
|
269
|
+
sessionCount
|
|
270
|
+
};
|
|
271
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ids.ts","../src/components.ts","../src/pageAt.ts","../src/session.ts","../src/createPaginator.ts","../src/PaginationSignal.ts","../src/respond.ts"],"sourcesContent":["import type { PaginationAction } from \"./types.js\";\n\nexport const PAGINATION_SIGNAL_NAME = \"pagination\";\n\nconst ACTION_SET = new Set<PaginationAction>([\"prev\", \"next\", \"dismiss\"]);\n\n/** Build a routable custom id: `stambha:pagination:{action}:{sessionId}`. */\nexport function paginationCustomId(action: PaginationAction, sessionId: string): string {\n return `stambha:${PAGINATION_SIGNAL_NAME}:${action}:${sessionId}`;\n}\n\n/** Suffix passed to {@link Signal.customId} / returned by {@link Signal.parseCustomId}. */\nexport function paginationSuffix(action: PaginationAction, sessionId: string): string {\n return `${action}:${sessionId}`;\n}\n\nexport function parsePaginationSuffix(\n suffix: string,\n): { action: PaginationAction; sessionId: string } | null {\n const sep = suffix.indexOf(\":\");\n if (sep <= 0) return null;\n const action = suffix.slice(0, sep);\n const sessionId = suffix.slice(sep + 1);\n if (!sessionId || !ACTION_SET.has(action as PaginationAction)) return null;\n return { action: action as PaginationAction, sessionId };\n}\n","import type { ReplyPayload } from \"@stambha/core\";\nimport { paginationCustomId } from \"./ids.js\";\nimport type { Page, ResolvedPaginatorLabels } from \"./types.js\";\n\nconst BUTTON = 2;\nconst ACTION_ROW = 1;\nconst STYLE_SECONDARY = 2;\nconst STYLE_DANGER = 4;\n\ninterface ButtonComponent {\n type: number;\n style: number;\n label: string;\n custom_id: string;\n disabled?: boolean;\n}\n\ninterface ActionRow {\n type: number;\n components: ButtonComponent[];\n}\n\nexport function buildPagePayload(\n page: Page,\n options: {\n sessionId: string;\n index: number;\n pageCount: number;\n labels: ResolvedPaginatorLabels;\n wrap: boolean;\n },\n): ReplyPayload {\n const { sessionId, index, pageCount, labels, wrap } = options;\n const atStart = index <= 0;\n const atEnd = index >= pageCount - 1;\n\n const prevDisabled = !wrap && atStart;\n const nextDisabled = !wrap && atEnd;\n\n const prev: ButtonComponent = {\n type: BUTTON,\n style: STYLE_SECONDARY,\n label: labels.prev,\n custom_id: paginationCustomId(\"prev\", sessionId),\n };\n if (prevDisabled) prev.disabled = true;\n\n const next: ButtonComponent = {\n type: BUTTON,\n style: STYLE_SECONDARY,\n label: labels.next,\n custom_id: paginationCustomId(\"next\", sessionId),\n };\n if (nextDisabled) next.disabled = true;\n\n const dismiss: ButtonComponent = {\n type: BUTTON,\n style: STYLE_DANGER,\n label: labels.dismiss,\n custom_id: paginationCustomId(\"dismiss\", sessionId),\n };\n\n const row: ActionRow = {\n type: ACTION_ROW,\n components: [prev, next, dismiss],\n };\n\n const payload: ReplyPayload = {\n components: [row],\n };\n if (page.content !== undefined) payload.content = page.content;\n if (page.embeds !== undefined) payload.embeds = page.embeds;\n return payload;\n}\n\n/** Same page content with controls removed (dismiss). */\nexport function buildDismissPayload(page: Page): ReplyPayload {\n const payload: ReplyPayload = {\n components: [],\n };\n if (page.content !== undefined) payload.content = page.content;\n if (page.embeds !== undefined) payload.embeds = page.embeds;\n return payload;\n}\n","import type { Page } from \"./types.js\";\n\n/** Sessions always have ≥1 page; return a safe page for the index. */\nexport function pageAt(pages: readonly Page[], index: number): Page {\n return pages[index] ?? pages[0] ?? {};\n}\n","import { randomBytes } from \"node:crypto\";\nimport type { PaginationSession } from \"./types.js\";\n\nconst sessions = new Map<string, PaginationSession>();\n\n/** Create a short opaque session id (fits Discord custom_id limits). */\nexport function createSessionId(): string {\n return randomBytes(6).toString(\"base64url\");\n}\n\nexport function setSession(id: string, session: PaginationSession): void {\n sessions.set(id, session);\n}\n\nexport function getSession(id: string): PaginationSession | undefined {\n const session = sessions.get(id);\n if (!session) return undefined;\n if (Date.now() > session.expiresAt) {\n sessions.delete(id);\n return undefined;\n }\n return session;\n}\n\nexport function deleteSession(id: string): void {\n sessions.delete(id);\n}\n\n/** Touch TTL after a successful interaction. */\nexport function touchSession(id: string): PaginationSession | undefined {\n const session = getSession(id);\n if (!session) return undefined;\n session.expiresAt = Date.now() + session.timeoutMs;\n return session;\n}\n\n/** Test helper — clear all sessions. */\nexport function clearSessions(): void {\n sessions.clear();\n}\n\n/** Test helper — current session count (includes unexpired only when read via get). */\nexport function sessionCount(): number {\n return sessions.size;\n}\n","import type { ReplyPayload } from \"@stambha/core\";\nimport { buildPagePayload } from \"./components.js\";\nimport { pageAt } from \"./pageAt.js\";\nimport { createSessionId, setSession } from \"./session.js\";\nimport type { Page, Paginator, PaginatorOptions, ResolvedPaginatorLabels } from \"./types.js\";\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60_000;\n\nconst DEFAULT_LABELS: ResolvedPaginatorLabels = {\n prev: \"Prev\",\n next: \"Next\",\n dismiss: \"Dismiss\",\n};\n\nfunction resolveLabels(labels?: PaginatorOptions[\"labels\"]): ResolvedPaginatorLabels {\n return {\n prev: labels?.prev ?? DEFAULT_LABELS.prev,\n next: labels?.next ?? DEFAULT_LABELS.next,\n dismiss: labels?.dismiss ?? DEFAULT_LABELS.dismiss,\n };\n}\n\nasync function resolvePages(pages: PaginatorOptions[\"pages\"]): Promise<Page[]> {\n const resolved = typeof pages === \"function\" ? await pages() : pages;\n const list = [...resolved];\n if (list.length === 0) {\n throw new Error(\"@stambha/pagination: pages must be a non-empty array\");\n }\n return list;\n}\n\n/**\n * Create an in-memory paginator session and return a helper that builds the\n * initial message payload (page content + prev/next/dismiss buttons).\n */\nexport async function createPaginator(options: PaginatorOptions): Promise<Paginator> {\n const pages = await resolvePages(options.pages);\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const wrap = options.wrap ?? false;\n const labels = resolveLabels(options.labels);\n const startAt = options.startAt ?? 0;\n const index = Math.min(Math.max(0, startAt), pages.length - 1);\n const sessionId = createSessionId();\n\n const session = {\n pages,\n index,\n expiresAt: Date.now() + timeoutMs,\n wrap,\n labels,\n timeoutMs,\n ...(options.userId !== undefined ? { userId: options.userId } : {}),\n };\n setSession(sessionId, session);\n\n return {\n sessionId,\n get index() {\n return session.index;\n },\n get pageCount() {\n return pages.length;\n },\n message(): ReplyPayload {\n const page = pageAt(pages, session.index);\n return buildPagePayload(page, {\n sessionId,\n index: session.index,\n pageCount: pages.length,\n labels,\n wrap,\n });\n },\n };\n}\n","import { type Registry, Signal, type SignalContext } from \"@stambha/core\";\nimport { buildDismissPayload, buildPagePayload } from \"./components.js\";\nimport { PAGINATION_SIGNAL_NAME, parsePaginationSuffix } from \"./ids.js\";\nimport { pageAt } from \"./pageAt.js\";\nimport { respondWithPage } from \"./respond.js\";\nimport { deleteSession, getSession, touchSession } from \"./session.js\";\n\n/**\n * Signal piece that handles `stambha:pagination:{action}:{sessionId}` button clicks.\n * Register under `src/signals/` (or construct and `registries.signals.register`).\n */\nexport class PaginationSignal extends Signal {\n constructor(registry: Registry<Signal>) {\n super(registry, {\n name: PAGINATION_SIGNAL_NAME,\n types: [\"button\"],\n });\n }\n\n async run(ctx: SignalContext): Promise<void> {\n const parsed = Signal.parseCustomId(ctx.customId);\n if (!parsed || parsed.name !== PAGINATION_SIGNAL_NAME) return;\n\n const parts = parsePaginationSuffix(parsed.suffix);\n if (!parts) {\n await ctx.replyEphemeral(\"This pagination control is invalid.\");\n return;\n }\n\n const session = getSession(parts.sessionId);\n if (!session) {\n await ctx.replyEphemeral(\"This paginator has expired. Run the command again.\");\n return;\n }\n\n if (session.userId !== undefined && session.userId !== ctx.userId) {\n await ctx.replyEphemeral(\"Only the user who opened this paginator can use it.\");\n return;\n }\n\n const restPort = this.client.restPort;\n const pageCount = session.pages.length;\n\n if (parts.action === \"dismiss\") {\n const page = pageAt(session.pages, session.index);\n deleteSession(parts.sessionId);\n await respondWithPage(ctx, restPort, buildDismissPayload(page));\n return;\n }\n\n let nextIndex = session.index;\n if (parts.action === \"prev\") {\n if (session.wrap) {\n nextIndex = (session.index - 1 + pageCount) % pageCount;\n } else if (session.index > 0) {\n nextIndex = session.index - 1;\n }\n } else if (parts.action === \"next\") {\n if (session.wrap) {\n nextIndex = (session.index + 1) % pageCount;\n } else if (session.index < pageCount - 1) {\n nextIndex = session.index + 1;\n }\n }\n\n session.index = nextIndex;\n touchSession(parts.sessionId);\n\n const page = pageAt(session.pages, session.index);\n await respondWithPage(\n ctx,\n restPort,\n buildPagePayload(page, {\n sessionId: parts.sessionId,\n index: session.index,\n pageCount,\n labels: session.labels,\n wrap: session.wrap,\n }),\n );\n }\n}\n","import {\n normalizeReplyData,\n type ReplyPayload,\n type RestPort,\n type SignalContext,\n} from \"@stambha/core\";\n\ninterface InteractionRaw {\n id?: string | null;\n token?: string | null;\n}\n\n/**\n * Prefer Discord interaction callback type 7 (UPDATE_MESSAGE) via {@link RestPort}.\n * Falls back to {@link SignalContext.reply} (type 4) when REST is unavailable.\n */\nexport async function respondWithPage(\n ctx: SignalContext,\n restPort: RestPort | null,\n payload: ReplyPayload,\n): Promise<void> {\n const raw = ctx.raw as InteractionRaw | null | undefined;\n const id = raw?.id ?? null;\n const token = raw?.token ?? null;\n\n if (restPort && id && token) {\n await restPort.request({\n method: \"POST\",\n route: `/interactions/${id}/${token}/callback`,\n body: {\n type: 7,\n data: normalizeReplyData(payload),\n },\n });\n return;\n }\n\n await ctx.reply(payload);\n}\n"],"mappings":"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","names":["page"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stambha/pagination",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Embed pagination with prev/next/dismiss buttons for Stambha Signals",
|
|
5
|
+
"author": "Mivaya",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/Mivaya/Stambha-plugins/tree/main/packages/pagination#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Mivaya/Stambha-plugins.git",
|
|
11
|
+
"directory": "packages/pagination"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Mivaya/Stambha/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"discord",
|
|
18
|
+
"discord-bot",
|
|
19
|
+
"stambha",
|
|
20
|
+
"pagination",
|
|
21
|
+
"signals",
|
|
22
|
+
"embeds"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./dist/index.cjs",
|
|
26
|
+
"module": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"require": "./dist/index.cjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@stambha/core": "^1.2.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@stambha/core": "^1.2.0",
|
|
44
|
+
"@types/node": "^22.13.4",
|
|
45
|
+
"tsup": "^8.3.6",
|
|
46
|
+
"typescript": "^5.7.3",
|
|
47
|
+
"vitest": "^3.0.5"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=20"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsup",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"test": "vitest run"
|
|
59
|
+
}
|
|
60
|
+
}
|