@sentroy-co/client-sdk 2.6.3 → 2.8.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/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,115 @@
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
+ - **Env Vault** — runtime env variable management (`@sentroy-co/client-sdk/vault`) — change a value in the dashboard, your app picks it up on the next read; no rebuild.
37
+ - **React drop-ins** — `<MediaManager />`, `<MediaManagerTrigger />` and `<EnvProvider>` / `useEnv()` (`@sentroy-co/client-sdk/react` + `@sentroy-co/client-sdk/vault/react`).
38
+ - **One client, two backends** — point at `https://sentroy.com` for the hosted platform, or your own deployment for self-hosted. Same API, same types.
39
+
40
+ ## Install
21
41
 
22
42
  ```bash
23
43
  npm install @sentroy-co/client-sdk
24
44
  ```
25
45
 
26
- ## Quick Start
46
+ ## First request
27
47
 
28
48
  ```ts
29
49
  import { Sentroy } from "@sentroy-co/client-sdk"
30
50
 
31
51
  const sentroy = new Sentroy({
32
- baseUrl: "https://sentroy.com",
52
+ baseUrl: "https://sentroy.com", // or your self-hosted URL
33
53
  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" },
148
- })
149
-
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",
165
- })
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
- ```
174
-
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.
180
-
181
- ```ts
182
- const suppressions = await sentroy.suppressions.list({
183
- domainId: "domain-id",
184
- reason: "complaint",
185
- page: 1,
186
- limit: 50,
187
- })
188
-
189
- const added = await sentroy.suppressions.add({
190
- email: "leaving@example.com",
191
- domainId: "domain-id",
192
- reason: "manual",
193
- })
194
-
195
- await sentroy.suppressions.remove(added.id)
196
- ```
197
-
198
- ### Webhooks
199
-
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.
203
-
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
211
-
212
- const all = await sentroy.webhooks.list() // every webhook
213
- const scoped = await sentroy.webhooks.list("domain-id")
214
-
215
- await sentroy.webhooks.update(webhook.id, { active: false })
216
- await sentroy.webhooks.delete(webhook.id)
217
- ```
218
-
219
- ### Logs
220
-
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.
223
-
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
- })
233
-
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" },
54
+ accessToken: "stk_...", // Dashboard → Admin → Access Tokens
263
55
  })
264
56
 
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
- ],
57
+ // Send your first transactional email
58
+ await sentroy.send.email({
59
+ from: "noreply@yourdomain.com",
60
+ to: ["customer@example.com"],
61
+ subject: "Welcome to Acme",
62
+ html: "<p>Glad you're here.</p>",
288
63
  })
289
64
  ```
290
65
 
291
- ### Buckets
292
-
293
- Storage is organized into **buckets** — isolated containers with their own
294
- visibility (public vs private) and usage counters.
66
+ ## Upload a file
295
67
 
296
68
  ```ts
297
- // List all buckets in the company
298
- const buckets = await sentroy.buckets.list()
69
+ const file = new File([blob], "invoice.pdf", { type: "application/pdf" })
299
70
 
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,
71
+ const media = await sentroy.media.upload({
72
+ bucketSlug: "invoices",
73
+ file,
308
74
  })
309
75
 
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 })
76
+ console.log(media.url) // signed URL, served from the CDN
315
77
  ```
316
78
 
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
- ```
79
+ 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.
363
80
 
364
- #### Thumbnail URL helpers
81
+ ## Env Vault
365
82
 
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:
83
+ Manage your env vars in the dashboard at [vault.sentroy.com](https://vault.sentroy.com), bootstrap your deploy with one token, and read values via a typed helper — no rebuild on change.
370
84
 
371
85
  ```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:
86
+ // server side
87
+ import { getEnv, getEnvOrThrow, preloadEnv } from "@sentroy-co/client-sdk/vault"
388
88
 
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
- }
89
+ await preloadEnv() // optional fail-fast at boot
90
+ const dbUrl = await getEnv("DATABASE_URL")
91
+ const turnstile = await getEnvOrThrow("BETTER_AUTH_TURNSTILE_SECRET")
417
92
  ```
418
93
 
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
94
  ```tsx
445
- "use client"
95
+ // React: SSR-injected provider + hook (no FOUC)
96
+ import { getPublicEnvs } from "@sentroy-co/client-sdk/vault"
97
+ import { EnvProvider, useEnv } from "@sentroy-co/client-sdk/vault/react"
446
98
 
447
- import { Sentroy } from "@sentroy-co/client-sdk"
448
- import { MediaManager } from "@sentroy-co/client-sdk/react"
99
+ // app/layout.tsx (server)
100
+ const envs = await getPublicEnvs()
101
+ return <EnvProvider envs={envs}>{children}</EnvProvider>
449
102
 
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
- }
103
+ // any "use client" component
104
+ const siteKey = useEnv("TURNSTILE_SITE_KEY")
467
105
  ```
468
106
 
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`:
107
+ Bootstrap is a single env: `SENTROY_ENV_API_KEY`. Public/private split is enforced server-side — the React hook only ever sees `public: true` variables. Full reference at [docs.sentroy.com/env-vault](https://docs.sentroy.com/env-vault).
509
108
 
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`.
109
+ ## Self-hosted vs hosted
526
110
 
527
- When you migrate to a different theme system later, change tokens in
528
- one place — every Tailwind utility resolves through your `globals.css`.
111
+ The SDK is identical in both modes. Only `baseUrl` changes:
529
112
 
530
- ### `MediaManagerTrigger`
113
+ | Mode | `baseUrl` |
114
+ |---|---|
115
+ | Sentroy Cloud (hosted) | `https://sentroy.com` |
116
+ | Self-hosted | `https://your-sentroy-host` |
531
117
 
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.
118
+ Pick the deployment that fits your compliance, latency and cost requirements. Migrate either direction without changing application code.
535
119
 
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.
120
+ ## Documentation
539
121
 
540
- ```tsx
541
- "use client"
542
-
543
- import { Sentroy } from "@sentroy-co/client-sdk"
544
- import { MediaManagerTrigger } from "@sentroy-co/client-sdk/react"
122
+ Full reference, interactive examples, and multi-language code samples (TypeScript, Go, Python, PHP, cURL) live at:
545
123
 
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
- ```
124
+ **[docs.sentroy.com](https://docs.sentroy.com)**
584
125
 
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
- ```
126
+ 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
127
 
680
128
  ## Requirements
681
129
 
@@ -683,12 +131,12 @@ import {
683
131
  - React 18+ (only if you import from `/react`)
684
132
  - Tailwind CSS in the host app (only for React components)
685
133
 
686
- ## Raw Documentation
134
+ ## For AI agents
687
135
 
688
- For AI agents and LLMs plain-text version of this document:
136
+ 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
137
 
690
138
  ```
691
- https://raw.githubusercontent.com/Sentroy-Co/client-sdk/refs/heads/main/typescript/README.md
139
+ https://raw.githubusercontent.com/Sentroy-Co/client-sdk/refs/heads/main/typescript/AGENTS.md
692
140
  ```
693
141
 
694
142
  ## License