@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.
Files changed (3) hide show
  1. package/AGENTS.md +692 -0
  2. package/README.md +53 -634
  3. 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)