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