@sentroy-co/client-sdk 2.6.3 → 2.6.4
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/AGENTS.md +692 -0
- package/README.md +53 -634
- package/package.json +4 -3
package/AGENTS.md
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://sentroy.com/business/sentroy-logo-light.png" alt="Sentroy" width="240" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h3 align="center">Sentroy Client SDK for TypeScript — Full Reference</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Comprehensive single-file reference covering every resource, parameter, and response shape.<br />
|
|
9
|
+
Optimised for AI agents and LLMs — feed the raw URL of this file into a context window and the assistant has the complete API surface.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<strong>Humans:</strong> start with the lean <a href="README.md">README</a> or browse <a href="https://docs.sentroy.com">docs.sentroy.com</a> for searchable, interactive docs.
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @sentroy-co/client-sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { Sentroy } from "@sentroy-co/client-sdk"
|
|
28
|
+
|
|
29
|
+
const sentroy = new Sentroy({
|
|
30
|
+
baseUrl: "https://sentroy.com",
|
|
31
|
+
companySlug: "my-company",
|
|
32
|
+
accessToken: "stk_...",
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> Access tokens can be created from **Admin > Access Tokens** in the Sentroy dashboard.
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### Domains
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// List all domains
|
|
44
|
+
const domains = await sentroy.domains.list()
|
|
45
|
+
|
|
46
|
+
// Get a single domain
|
|
47
|
+
const domain = await sentroy.domains.get("domain-id")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Mailboxes
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// List all mailbox accounts
|
|
54
|
+
const mailboxes = await sentroy.mailboxes.list()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Templates
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// List all templates
|
|
61
|
+
const templates = await sentroy.templates.list()
|
|
62
|
+
|
|
63
|
+
// Get a template by ID
|
|
64
|
+
const template = await sentroy.templates.get("template-id")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Templates support multiple languages via `LocalizedString`. A field can be a plain string or an object keyed by language code:
|
|
68
|
+
|
|
69
|
+
```jsonc
|
|
70
|
+
// Example template response
|
|
71
|
+
{
|
|
72
|
+
"id": "b3f1a2c4-...",
|
|
73
|
+
"name": { "en": "Welcome Email", "tr": "Hosgeldin E-postasi" },
|
|
74
|
+
"subject": { "en": "Welcome, {{name}}!", "tr": "Hosgeldin, {{name}}!" },
|
|
75
|
+
"mjmlBody": { "en": "<mjml>...</mjml>", "tr": "<mjml>...</mjml>" },
|
|
76
|
+
"variables": ["name", "company"],
|
|
77
|
+
"domainId": "a1b2c3d4-...",
|
|
78
|
+
"domainName": "example.com",
|
|
79
|
+
"createdAt": "2026-01-15T10:30:00.000Z",
|
|
80
|
+
"updatedAt": "2026-04-10T14:22:00.000Z"
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Use the `variables` array to know which placeholders (`{{name}}`, `{{company}}`) the template expects.
|
|
85
|
+
|
|
86
|
+
### Inbox
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// List messages
|
|
90
|
+
const messages = await sentroy.inbox.list({
|
|
91
|
+
mailbox: "info@example.com",
|
|
92
|
+
folder: "INBOX",
|
|
93
|
+
page: 1,
|
|
94
|
+
limit: 20,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Get a single message
|
|
98
|
+
const message = await sentroy.inbox.get(1234, {
|
|
99
|
+
mailbox: "info@example.com",
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// List IMAP folders
|
|
103
|
+
const folders = await sentroy.inbox.listFolders("info@example.com")
|
|
104
|
+
|
|
105
|
+
// Get a thread by subject
|
|
106
|
+
const thread = await sentroy.inbox.getThread("Re: Project update", "info@example.com")
|
|
107
|
+
|
|
108
|
+
// Mark as read / unread
|
|
109
|
+
await sentroy.inbox.markAsRead(1234, { mailbox: "info@example.com" })
|
|
110
|
+
await sentroy.inbox.markAsUnread(1234, { mailbox: "info@example.com" })
|
|
111
|
+
|
|
112
|
+
// Move message
|
|
113
|
+
await sentroy.inbox.move(1234, "Trash", {
|
|
114
|
+
from: "INBOX",
|
|
115
|
+
mailbox: "info@example.com",
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Delete message
|
|
119
|
+
await sentroy.inbox.delete(1234, { mailbox: "info@example.com" })
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Audience
|
|
123
|
+
|
|
124
|
+
Manage contacts and audience lists from the SDK. Useful for building
|
|
125
|
+
your own newsletter signup form, syncing customers from another system,
|
|
126
|
+
or assembling segments for a campaign.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// List + paginate contacts (filter by status / tags)
|
|
130
|
+
const { contacts, total, page, limit } = await sentroy.audience.contacts.list({
|
|
131
|
+
page: 1,
|
|
132
|
+
limit: 50,
|
|
133
|
+
status: "active",
|
|
134
|
+
tags: ["customer", "vip"],
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Email-prefix autocomplete (capped at 10 server-side)
|
|
138
|
+
const matches = await sentroy.audience.contacts.search("alex@")
|
|
139
|
+
|
|
140
|
+
// Create a contact
|
|
141
|
+
const contact = await sentroy.audience.contacts.create({
|
|
142
|
+
email: "user@example.com",
|
|
143
|
+
name: "Jane Doe",
|
|
144
|
+
tags: ["beta-tester"],
|
|
145
|
+
metadata: { signupSource: "landing-2026-q2" },
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// Patch — pass any subset of fields. Use `status` to mark unsubscribed.
|
|
149
|
+
await sentroy.audience.contacts.update(contact.id, { tags: ["customer"] })
|
|
150
|
+
|
|
151
|
+
// Soft-delete (sets status: "unsubscribed" — record is preserved)
|
|
152
|
+
await sentroy.audience.contacts.delete(contact.id)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Audience lists are simple groupings; a single contact can belong to many.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
// CRUD audience lists
|
|
159
|
+
const lists = await sentroy.audience.lists.list()
|
|
160
|
+
const list = await sentroy.audience.lists.create({
|
|
161
|
+
name: "Newsletter — May 2026",
|
|
162
|
+
description: "Opt-ins from the homepage form",
|
|
163
|
+
})
|
|
164
|
+
await sentroy.audience.lists.delete(list.id)
|
|
165
|
+
|
|
166
|
+
// Membership — scoped to a single list id
|
|
167
|
+
const members = sentroy.audience.lists.members(list.id)
|
|
168
|
+
await members.add(contact.id)
|
|
169
|
+
const inList = await members.list()
|
|
170
|
+
await members.remove(contact.id)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Suppressions
|
|
174
|
+
|
|
175
|
+
Suppressed addresses are skipped at send time. Bounces and complaints
|
|
176
|
+
are added automatically; the API is for manually honoring off-platform
|
|
177
|
+
opt-outs or removing a stale entry.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const suppressions = await sentroy.suppressions.list({
|
|
181
|
+
domainId: "domain-id",
|
|
182
|
+
reason: "complaint",
|
|
183
|
+
page: 1,
|
|
184
|
+
limit: 50,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const added = await sentroy.suppressions.add({
|
|
188
|
+
email: "leaving@example.com",
|
|
189
|
+
domainId: "domain-id",
|
|
190
|
+
reason: "manual",
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
await sentroy.suppressions.remove(added.id)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Webhooks
|
|
197
|
+
|
|
198
|
+
Subscribe to delivery events on a per-domain basis. The `secret`
|
|
199
|
+
returned at create time signs every delivery — store it and verify the
|
|
200
|
+
HMAC on your endpoint.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const webhook = await sentroy.webhooks.create({
|
|
204
|
+
url: "https://example.com/webhooks/sentroy",
|
|
205
|
+
events: ["sent", "bounced", "opened", "clicked", "unsubscribed"],
|
|
206
|
+
domainId: "domain-id",
|
|
207
|
+
})
|
|
208
|
+
console.log(webhook.secret) // Returned ONCE — store it now
|
|
209
|
+
|
|
210
|
+
const all = await sentroy.webhooks.list() // every webhook
|
|
211
|
+
const scoped = await sentroy.webhooks.list("domain-id")
|
|
212
|
+
|
|
213
|
+
await sentroy.webhooks.update(webhook.id, { active: false })
|
|
214
|
+
await sentroy.webhooks.delete(webhook.id)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Logs
|
|
218
|
+
|
|
219
|
+
Query the mail log to debug delivery issues, surface per-message status
|
|
220
|
+
in your own UI, or build a customer-facing activity timeline.
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
const logs = await sentroy.logs.list({
|
|
224
|
+
status: "bounced",
|
|
225
|
+
domainId: "domain-id",
|
|
226
|
+
from: "2026-05-01T00:00:00Z",
|
|
227
|
+
to: "2026-05-31T23:59:59Z",
|
|
228
|
+
page: 1,
|
|
229
|
+
limit: 100,
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const log = await sentroy.logs.get(logs[0].id)
|
|
233
|
+
console.log(log.openedAt, log.clickedAt) // tracking timestamps if enabled
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Send Email
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
// Send with a template (uses default language)
|
|
240
|
+
const result = await sentroy.send.email({
|
|
241
|
+
to: "user@example.com",
|
|
242
|
+
from: "info@example.com",
|
|
243
|
+
subject: "Welcome!",
|
|
244
|
+
domainId: "domain-id",
|
|
245
|
+
templateId: "template-id",
|
|
246
|
+
variables: {
|
|
247
|
+
name: "John",
|
|
248
|
+
company: "Acme",
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
// Send with a specific language
|
|
253
|
+
const result = await sentroy.send.email({
|
|
254
|
+
to: "user@example.com",
|
|
255
|
+
from: "info@example.com",
|
|
256
|
+
subject: "Hosgeldin!",
|
|
257
|
+
domainId: "domain-id",
|
|
258
|
+
templateId: "template-id",
|
|
259
|
+
lang: "tr",
|
|
260
|
+
variables: { name: "Ahmet" },
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// Send with raw HTML
|
|
264
|
+
const result = await sentroy.send.email({
|
|
265
|
+
to: ["user1@example.com", "user2@example.com"],
|
|
266
|
+
from: "info@example.com",
|
|
267
|
+
subject: "Hello",
|
|
268
|
+
domainId: "domain-id",
|
|
269
|
+
html: "<h1>Hello World</h1>",
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// Send with attachments
|
|
273
|
+
const result = await sentroy.send.email({
|
|
274
|
+
to: "user@example.com",
|
|
275
|
+
from: "info@example.com",
|
|
276
|
+
subject: "Invoice",
|
|
277
|
+
domainId: "domain-id",
|
|
278
|
+
html: "<p>Please find your invoice attached.</p>",
|
|
279
|
+
attachments: [
|
|
280
|
+
{
|
|
281
|
+
filename: "invoice.pdf",
|
|
282
|
+
content: base64String,
|
|
283
|
+
contentType: "application/pdf",
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
})
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Buckets
|
|
290
|
+
|
|
291
|
+
Storage is organized into **buckets** — isolated containers with their own
|
|
292
|
+
visibility (public vs private) and usage counters.
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
// List all buckets in the company
|
|
296
|
+
const buckets = await sentroy.buckets.list()
|
|
297
|
+
|
|
298
|
+
// Get a single bucket by its slug
|
|
299
|
+
const bucket = await sentroy.buckets.get("product-assets")
|
|
300
|
+
|
|
301
|
+
// Create a bucket (slug auto-derived from name if omitted)
|
|
302
|
+
const created = await sentroy.buckets.create({
|
|
303
|
+
name: "User Uploads",
|
|
304
|
+
description: "Avatars and profile media",
|
|
305
|
+
isPublic: false,
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Update a bucket — toggling isPublic cascades to every file's ACL
|
|
309
|
+
await sentroy.buckets.update("product-assets", { isPublic: true })
|
|
310
|
+
|
|
311
|
+
// Delete a bucket (409 if it has files; use force to purge everything)
|
|
312
|
+
await sentroy.buckets.delete("product-assets", { force: true })
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Media
|
|
316
|
+
|
|
317
|
+
Upload, list, download, and delete files inside a bucket. The same token
|
|
318
|
+
that authorizes mail calls also authorizes storage calls.
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
// List files in a bucket
|
|
322
|
+
const { items, total } = await sentroy.media.list("product-assets", {
|
|
323
|
+
type: "image",
|
|
324
|
+
limit: 50,
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// Get a single media record
|
|
328
|
+
const media = await sentroy.media.get("product-assets", mediaId)
|
|
329
|
+
|
|
330
|
+
// Upload — browser (File from <input>)
|
|
331
|
+
const input = document.querySelector<HTMLInputElement>("input[type=file]")!
|
|
332
|
+
const file = input.files![0]
|
|
333
|
+
const uploaded = await sentroy.media.upload("product-assets", {
|
|
334
|
+
body: file,
|
|
335
|
+
folder: "products",
|
|
336
|
+
tags: ["v1", "cover"],
|
|
337
|
+
})
|
|
338
|
+
console.log(uploaded.url) // Public URL from the CDN
|
|
339
|
+
|
|
340
|
+
// Upload — Node.js (Blob from fs)
|
|
341
|
+
import { openAsBlob } from "node:fs"
|
|
342
|
+
const blob = await openAsBlob("./photo.jpg")
|
|
343
|
+
const uploaded = await sentroy.media.upload("product-assets", {
|
|
344
|
+
body: blob,
|
|
345
|
+
filename: "photo.jpg",
|
|
346
|
+
isPublic: true,
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
// Download — streams from the storage backend; works for both public
|
|
350
|
+
// and private buckets (auth-gated for private).
|
|
351
|
+
const blob = await sentroy.media.download("product-assets", mediaId)
|
|
352
|
+
// Variant: ask for a pre-generated thumbnail width (falls back to
|
|
353
|
+
// original if that size wasn't generated for this file).
|
|
354
|
+
const thumb = await sentroy.media.download("product-assets", mediaId, {
|
|
355
|
+
quality: 500,
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
// Delete — removes S3 objects (original + thumbnails) + Media record
|
|
359
|
+
await sentroy.media.delete("product-assets", mediaId)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### Thumbnail URL helpers
|
|
363
|
+
|
|
364
|
+
When you upload an image, the CDN auto-generates several thumbnail
|
|
365
|
+
sizes (`media.imageMeta.thumbnails`). Showing the original 4000-px JPG
|
|
366
|
+
in a 56-px avatar wastes bandwidth and slows render. Use these helpers
|
|
367
|
+
to pick the right URL for the display target:
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
import {
|
|
371
|
+
pickThumbnailUrl,
|
|
372
|
+
pickPresetThumbnailUrl,
|
|
373
|
+
THUMBNAIL_PRESETS,
|
|
374
|
+
} from "@sentroy-co/client-sdk"
|
|
375
|
+
|
|
376
|
+
// Manual target (px) — pass display size * 2 for retina
|
|
377
|
+
const avatarUrl = pickThumbnailUrl(media, 56 * 2)
|
|
378
|
+
|
|
379
|
+
// Semantic preset — avatar / card / preview / hero
|
|
380
|
+
const cardUrl = pickPresetThumbnailUrl(media, "card") // → ~500px
|
|
381
|
+
const previewUrl = pickPresetThumbnailUrl(media, "preview") // → ~960px
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
The helper picks the smallest thumbnail that still **covers** the
|
|
385
|
+
target (so you never upscale), then falls back through:
|
|
386
|
+
|
|
387
|
+
1. `thumbnail.url` if the backend exposed it directly,
|
|
388
|
+
2. CDN-prefix + `thumbnail.fileName` derived from `media.url`,
|
|
389
|
+
3. proxy `media.downloadUrl?quality=N` for private buckets,
|
|
390
|
+
4. `media.url` / `media.downloadUrl` if no thumbnails exist
|
|
391
|
+
(non-image, or image upload before thumbnails were generated).
|
|
392
|
+
|
|
393
|
+
Returns `undefined` only when the media has no public URL at all.
|
|
394
|
+
|
|
395
|
+
| Preset | Target px | Use case |
|
|
396
|
+
|-------------|-----------|----------|
|
|
397
|
+
| `avatar` | 128 | Round chips, 28-64 px display @2x |
|
|
398
|
+
| `card` | 500 | Grid / list cards, 200-300 px |
|
|
399
|
+
| `preview` | 960 | Modal / detail view |
|
|
400
|
+
| `hero` | 1600 | Full-bleed hero, edge cases |
|
|
401
|
+
|
|
402
|
+
## Error Handling
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
import { Sentroy, SentroyError } from "@sentroy-co/client-sdk"
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
await sentroy.send.email({ ... })
|
|
409
|
+
} catch (err) {
|
|
410
|
+
if (err instanceof SentroyError) {
|
|
411
|
+
console.error(err.statusCode) // 401, 403, 500, etc.
|
|
412
|
+
console.error(err.message) // Human-readable error
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
## Configuration
|
|
418
|
+
|
|
419
|
+
| Option | Type | Required | Description |
|
|
420
|
+
|--------|------|----------|-------------|
|
|
421
|
+
| `baseUrl` | `string` | Yes | Sentroy instance URL (e.g. `https://sentroy.com`) |
|
|
422
|
+
| `companySlug` | `string` | Yes | Your company slug |
|
|
423
|
+
| `accessToken` | `string` | Yes | Access token (`stk_...`) |
|
|
424
|
+
| `timeout` | `number` | No | Request timeout in ms (default: `30000`) |
|
|
425
|
+
|
|
426
|
+
## React components (`@sentroy-co/client-sdk/react`)
|
|
427
|
+
|
|
428
|
+
Optional subpath. Only loaded if you import it; React + react-dom are
|
|
429
|
+
declared as **optional peer dependencies** so server-only consumers
|
|
430
|
+
don't need to install them.
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
npm install react react-dom
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### `MediaManager`
|
|
437
|
+
|
|
438
|
+
Drop-in storage browser/uploader for end-user apps. Talks to the same
|
|
439
|
+
Sentroy client you already use; renders Tailwind classes (host app's
|
|
440
|
+
Tailwind setup is reused — the package ships no styles).
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
"use client"
|
|
444
|
+
|
|
445
|
+
import { Sentroy } from "@sentroy-co/client-sdk"
|
|
446
|
+
import { MediaManager } from "@sentroy-co/client-sdk/react"
|
|
447
|
+
|
|
448
|
+
const client = new Sentroy({
|
|
449
|
+
baseUrl: "https://sentroy.com",
|
|
450
|
+
companySlug: "my-company",
|
|
451
|
+
accessToken: "stk_...",
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
export default function Page() {
|
|
455
|
+
return (
|
|
456
|
+
<MediaManager
|
|
457
|
+
client={client}
|
|
458
|
+
multiple
|
|
459
|
+
accept="image/*"
|
|
460
|
+
onChange={(selected) => console.log(selected)}
|
|
461
|
+
onSelect={(selected) => console.log("confirmed:", selected)}
|
|
462
|
+
/>
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
#### Features
|
|
468
|
+
|
|
469
|
+
- Bucket selector (auto-picks first if `bucketSlug` not provided)
|
|
470
|
+
- Search (filename) + file-type filter (image / video / audio / pdf / doc / archive / code)
|
|
471
|
+
- Upload via button **and** drag-and-drop
|
|
472
|
+
- Single or multi selection (`multiple` prop)
|
|
473
|
+
- `initialValue` accepts `Media[]` or `string[]` (id list) — pre-selected
|
|
474
|
+
on mount, fires `onChange` immediately so parent state stays in sync
|
|
475
|
+
- Press `Space` while a card is selected → opens it in fullscreen
|
|
476
|
+
**Lightbox** (image / video / audio render natively, others get a
|
|
477
|
+
download fallback). `Esc` closes, `←/→` step through siblings
|
|
478
|
+
- Detail pane on the right (large screens) — preview, metadata,
|
|
479
|
+
delete, "Use selection" CTA when `onSelect` provided
|
|
480
|
+
|
|
481
|
+
#### Props
|
|
482
|
+
|
|
483
|
+
| Prop | Type | Required | Description |
|
|
484
|
+
|----------------------|-------------------------------------------------------|:-:|:--|
|
|
485
|
+
| `client` | `Sentroy` | Yes | The configured client instance |
|
|
486
|
+
| `bucketSlug` | `string` | | Initial bucket; default = first one in the list |
|
|
487
|
+
| `multiple` | `boolean` | | Allow multi-selection. Default `false` |
|
|
488
|
+
| `maxItems` | `number` | | Cap for multi-mode. New selections are silently blocked once reached. Ignored when `multiple=false` |
|
|
489
|
+
| `accept` | `string` | | File type filter — applies to upload **and** the grid. Same syntax as `<input accept>`: `"image/*"`, `"image/png,image/jpeg"`, `".pdf,.docx"`, comma-separated combos |
|
|
490
|
+
| `initialValue` | `Array<Media \| string>` | | Pre-selected items (objects or ids) |
|
|
491
|
+
| `onChange` | `(selected: Media[]) => void` | | Fires on every selection change |
|
|
492
|
+
| `onSelect` | `(selected: Media[]) => void` | | Fires on confirm — picker dialogs use this |
|
|
493
|
+
| `bucketFilter` | `(b: Bucket) => boolean` | | Filter the bucket dropdown — hide system buckets |
|
|
494
|
+
| `showDetailsPane` | `boolean` | | Default `true` |
|
|
495
|
+
| `showBucketSelector` | `boolean` | | Default `true` |
|
|
496
|
+
| `className` | `string` | | Root wrapper class |
|
|
497
|
+
| `classNames` | `MediaManagerClassNames` | | Per-region class overrides (see theming) |
|
|
498
|
+
|
|
499
|
+
#### Theming
|
|
500
|
+
|
|
501
|
+
The component uses Tailwind utility classes that consume your design
|
|
502
|
+
tokens (`bg-background`, `text-foreground`, `border-border`,
|
|
503
|
+
`text-muted-foreground`, `bg-muted`, etc.). Drop-in usage in any
|
|
504
|
+
shadcn-style codebase needs no extra setup.
|
|
505
|
+
|
|
506
|
+
For finer control, override individual sections via `classNames`:
|
|
507
|
+
|
|
508
|
+
```tsx
|
|
509
|
+
<MediaManager
|
|
510
|
+
client={client}
|
|
511
|
+
className="h-[600px] rounded-2xl border-purple-200"
|
|
512
|
+
classNames={{
|
|
513
|
+
toolbar: "bg-purple-50",
|
|
514
|
+
uploadButton: "bg-purple-600 text-white",
|
|
515
|
+
cardSelected: "ring-purple-400 border-purple-400",
|
|
516
|
+
grid: "sm:grid-cols-2 md:grid-cols-3", // override grid density
|
|
517
|
+
}}
|
|
518
|
+
/>
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Available keys: `root`, `toolbar`, `searchInput`, `filterSelect`,
|
|
522
|
+
`uploadButton`, `bucketSelect`, `grid`, `card`, `cardSelected`,
|
|
523
|
+
`thumbnail`, `cardMeta`, `empty`, `details`, `dropZoneOverlay`.
|
|
524
|
+
|
|
525
|
+
When you migrate to a different theme system later, change tokens in
|
|
526
|
+
one place — every Tailwind utility resolves through your `globals.css`.
|
|
527
|
+
|
|
528
|
+
### `MediaManagerTrigger`
|
|
529
|
+
|
|
530
|
+
A wrapper that turns **any** clickable element into a media picker — when
|
|
531
|
+
the user clicks the `trigger`, a portal-rendered modal opens with
|
|
532
|
+
`MediaManager` inside, and `onSelect` fires with the confirmed selection.
|
|
533
|
+
|
|
534
|
+
The use case: you don't want a giant manager taking up real estate on your
|
|
535
|
+
profile/settings page — you just want a "Change avatar" button (or even
|
|
536
|
+
a clickable avatar thumbnail) that pops the picker on demand.
|
|
537
|
+
|
|
538
|
+
```tsx
|
|
539
|
+
"use client"
|
|
540
|
+
|
|
541
|
+
import { Sentroy } from "@sentroy-co/client-sdk"
|
|
542
|
+
import { MediaManagerTrigger } from "@sentroy-co/client-sdk/react"
|
|
543
|
+
|
|
544
|
+
const client = new Sentroy({
|
|
545
|
+
baseUrl: "https://sentroy.com",
|
|
546
|
+
companySlug: "my-company",
|
|
547
|
+
accessToken: "stk_...",
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
export function AvatarPicker({
|
|
551
|
+
current,
|
|
552
|
+
onChange,
|
|
553
|
+
}: {
|
|
554
|
+
current: string | null
|
|
555
|
+
onChange: (url: string) => void
|
|
556
|
+
}) {
|
|
557
|
+
return (
|
|
558
|
+
<MediaManagerTrigger
|
|
559
|
+
client={client}
|
|
560
|
+
maxItems={1}
|
|
561
|
+
accept="image/*"
|
|
562
|
+
title="Choose your avatar"
|
|
563
|
+
description="Pick an existing image or upload a new one."
|
|
564
|
+
trigger={
|
|
565
|
+
<button className="rounded-full ring-2 ring-border hover:ring-primary">
|
|
566
|
+
{current ? (
|
|
567
|
+
<img src={current} alt="" className="size-10 rounded-full" />
|
|
568
|
+
) : (
|
|
569
|
+
<span className="grid size-10 place-items-center rounded-full bg-muted text-xs">
|
|
570
|
+
?
|
|
571
|
+
</span>
|
|
572
|
+
)}
|
|
573
|
+
</button>
|
|
574
|
+
}
|
|
575
|
+
onSelect={(media) => {
|
|
576
|
+
if (media[0]?.url) onChange(media[0].url)
|
|
577
|
+
}}
|
|
578
|
+
/>
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
#### Multi-select with cap
|
|
584
|
+
|
|
585
|
+
```tsx
|
|
586
|
+
<MediaManagerTrigger
|
|
587
|
+
client={client}
|
|
588
|
+
maxItems={5}
|
|
589
|
+
accept="image/*,video/*"
|
|
590
|
+
trigger={<Button>Add gallery items</Button>}
|
|
591
|
+
onSelect={(media) => setGallery(media)}
|
|
592
|
+
/>
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
`maxItems > 1` automatically enables multi-mode. Once the user reaches
|
|
596
|
+
the cap, additional clicks on unselected cards are silently no-op'd —
|
|
597
|
+
they have to deselect something to swap.
|
|
598
|
+
|
|
599
|
+
#### Controlled mode
|
|
600
|
+
|
|
601
|
+
If you want the parent to drive open/close (e.g. opening from a context
|
|
602
|
+
menu), pass `open` + `onOpenChange`. The `trigger` is still rendered so
|
|
603
|
+
its click also opens the modal — to render only the modal, pass an empty
|
|
604
|
+
fragment for `trigger`.
|
|
605
|
+
|
|
606
|
+
```tsx
|
|
607
|
+
const [open, setOpen] = useState(false)
|
|
608
|
+
|
|
609
|
+
<MediaManagerTrigger
|
|
610
|
+
client={client}
|
|
611
|
+
open={open}
|
|
612
|
+
onOpenChange={setOpen}
|
|
613
|
+
trigger={<></>}
|
|
614
|
+
onSelect={(media) => { /* … */ }}
|
|
615
|
+
/>
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
#### Props
|
|
619
|
+
|
|
620
|
+
| Prop | Type | Required | Description |
|
|
621
|
+
|--------------------|-----------------------------------|:-:|:--|
|
|
622
|
+
| `client` | `Sentroy` | Yes | Same client you pass to `MediaManager` |
|
|
623
|
+
| `trigger` | `ReactNode` | Yes | The clickable element. Wrapped in `<span role="button">` with click + keyboard (Enter / Space) handlers |
|
|
624
|
+
| `onSelect` | `(selected: Media[]) => void` | Yes | Fires when user confirms; modal auto-closes |
|
|
625
|
+
| `maxItems` | `number` | | `1` = single (default), `>1` = multi up to cap |
|
|
626
|
+
| `accept` | `string` | | Same `<input accept>` syntax — applies to upload **and** grid filter |
|
|
627
|
+
| `title` | `string` | | Modal heading. Default `"Select media"` |
|
|
628
|
+
| `description` | `string` | | Subheading under the title |
|
|
629
|
+
| `open` | `boolean` | | Controlled open state |
|
|
630
|
+
| `onOpenChange` | `(open: boolean) => void` | | Controlled change handler |
|
|
631
|
+
| `disabled` | `boolean` | | Trigger ignores clicks; visual disabled state |
|
|
632
|
+
| `confirmLabel` | `string` | | Default `"Use selection"` |
|
|
633
|
+
| `cancelLabel` | `string` | | Default `"Cancel"` |
|
|
634
|
+
| `modalClassName` | `string` | | Class on the modal panel |
|
|
635
|
+
| `triggerClassName` | `string` | | Class on the trigger wrapper span |
|
|
636
|
+
| … | rest of `MediaManagerProps` | | `bucketSlug`, `bucketFilter`, `showDetailsPane`, `classNames`, etc. forwarded to the inner `MediaManager` |
|
|
637
|
+
|
|
638
|
+
The modal renders into `document.body` via `react-dom` portal, so it
|
|
639
|
+
escapes parent `overflow:hidden` / transform stacking contexts. `Esc`
|
|
640
|
+
closes; backdrop click closes; body scroll is locked while open.
|
|
641
|
+
|
|
642
|
+
#### `Lightbox` (standalone)
|
|
643
|
+
|
|
644
|
+
Exported separately so you can use it outside `MediaManager` (e.g. in
|
|
645
|
+
a feed view):
|
|
646
|
+
|
|
647
|
+
```tsx
|
|
648
|
+
import { Lightbox } from "@sentroy-co/client-sdk/react"
|
|
649
|
+
|
|
650
|
+
const [active, setActive] = useState<Media | null>(null)
|
|
651
|
+
|
|
652
|
+
return (
|
|
653
|
+
<>
|
|
654
|
+
{/* …trigger… */}
|
|
655
|
+
{active && (
|
|
656
|
+
<Lightbox media={active} onClose={() => setActive(null)} />
|
|
657
|
+
)}
|
|
658
|
+
</>
|
|
659
|
+
)
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
Image / video / audio rendered inline; everything else gets a download
|
|
663
|
+
button. `Esc` closes, optional `onPrev` / `onNext` add ←/→ navigation.
|
|
664
|
+
|
|
665
|
+
#### Helpers
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
import {
|
|
669
|
+
cn, // tiny class joiner
|
|
670
|
+
formatBytes, // 1234 → "1.21 KB"
|
|
671
|
+
detectKind, // image | video | audio | pdf | doc | archive | code | other
|
|
672
|
+
matchAccept, // matchAccept(file, "image/*,.pdf") → boolean
|
|
673
|
+
KIND_LABELS,
|
|
674
|
+
type MediaKind,
|
|
675
|
+
} from "@sentroy-co/client-sdk/react"
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
## Requirements
|
|
679
|
+
|
|
680
|
+
- Node.js 18+ (uses native `fetch`)
|
|
681
|
+
- React 18+ (only if you import from `/react`)
|
|
682
|
+
- Tailwind CSS in the host app (only for React components)
|
|
683
|
+
|
|
684
|
+
## Raw URL — for LLM/agent context windows
|
|
685
|
+
|
|
686
|
+
```
|
|
687
|
+
https://raw.githubusercontent.com/Sentroy-Co/client-sdk/refs/heads/main/typescript/AGENTS.md
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
## License
|
|
691
|
+
|
|
692
|
+
[MIT](LICENSE)
|