React components
Optional subpath of @sentroy-co/client-sdk/react. React and react-dom are declared as optional peer dependencies— server-only consumers don't need to install them.
npm install react react-domMediaManager#
Drop-in storage browser and uploader. Talks to the same Sentroy client you already configured. Renders Tailwind classes that consume your design tokens — no CSS to import.
"use client"
import { Sentroy } from "@sentroy-co/client-sdk"
import { MediaManager } from "@sentroy-co/client-sdk/react"
const client = new Sentroy({
baseUrl: "https://sentroy.com",
companySlug: "my-company",
accessToken: "stk_...",
})
export default function Page() {
return (
<MediaManager
client={client}
multiple
accept="image/*"
onChange={(selected) => console.log(selected)}
onSelect={(selected) => console.log("confirmed:", selected)}
/>
)
}Features
- Bucket selector (auto-picks first if
bucketSlugnot provided) - Search (filename) + file-type filter (image / video / audio / pdf / doc / archive / code)
- Upload via button and drag-and-drop
- Single or multi selection (
multipleprop) initialValueacceptsMedia[]orstring[](id list) — pre-selected on mount, firesonChangeimmediately so parent state stays in sync- Press
Spacewhile a card is selected → opens it in fullscreen Lightbox.Esccloses,←/→step through siblings - Detail pane on the right (large screens) — preview, metadata, delete, "Use selection" CTA when
onSelectprovided
Props
| Name | Type | Description |
|---|---|---|
| clientrequired | Sentroy | Configured client instance |
| bucketSlug | string | Initial bucket; default = first one in the list |
| multiple | boolean | Allow multi-selection. Default false |
| maxItems | number | Cap for multi-mode. New selections are silently blocked once reached. Ignored when multiple=false |
| accept | string | File type filter — same syntax as <input accept>: "image/*", ".pdf,.docx", combos |
| initialValue | Array<Media | string> | Pre-selected items (objects or ids) |
| onChange | (selected: Media[]) => void | Fires on every selection change |
| onSelect | (selected: Media[]) => void | Fires on confirm — picker dialogs use this |
| bucketFilter | (b: Bucket) => boolean | Filter the bucket dropdown — hide system buckets |
| showDetailsPane | boolean | Default true |
| showBucketSelector | boolean | Default true |
| className | string | Root wrapper class |
| classNames | MediaManagerClassNames | Per-region class overrides (see theming) |
Theming
The component uses Tailwind utility classes that consume your design tokens (bg-background, text-foreground, border-border, text-muted-foreground, bg-muted, etc.). Drop-in usage in any shadcn-style codebase needs no extra setup.
For finer control, override individual sections via classNames:
<MediaManager
client={client}
className="h-[600px] rounded-2xl border-purple-200"
classNames={{
toolbar: "bg-purple-50",
uploadButton: "bg-purple-600 text-white",
cardSelected: "ring-purple-400 border-purple-400",
grid: "sm:grid-cols-2 md:grid-cols-3",
}}
/>Available keys: root, toolbar, searchInput, filterSelect, uploadButton, bucketSelect, grid, card, cardSelected, thumbnail, cardMeta, empty, details, dropZoneOverlay.
MediaManagerTrigger#
A wrapper that turns any clickable element into a media picker. When the user clicks the trigger, a portal-rendered modal opens with MediaManager inside, and onSelect fires with the confirmed selection.
The use case: you don't want a giant manager taking up real estate on your settings page — you just want a "Change avatar" button (or even a clickable avatar thumbnail) that pops the picker on demand.
"use client"
import { Sentroy } from "@sentroy-co/client-sdk"
import { MediaManagerTrigger } from "@sentroy-co/client-sdk/react"
const client = new Sentroy({
baseUrl: "https://sentroy.com",
companySlug: "my-company",
accessToken: "stk_...",
})
export function AvatarPicker({
current,
onChange,
}: {
current: string | null
onChange: (url: string) => void
}) {
return (
<MediaManagerTrigger
client={client}
maxItems={1}
accept="image/*"
title="Choose your avatar"
description="Pick an existing image or upload a new one."
trigger={
<button className="rounded-full ring-2 ring-border hover:ring-primary">
{current ? (
<img src={current} alt="" className="size-10 rounded-full" />
) : (
<span className="grid size-10 place-items-center rounded-full bg-muted text-xs">
?
</span>
)}
</button>
}
onSelect={(media) => {
if (media[0]?.url) onChange(media[0].url)
}}
/>
)
}Multi-select with cap
<MediaManagerTrigger
client={client}
maxItems={5}
accept="image/*,video/*"
trigger={<Button>Add gallery items</Button>}
onSelect={(media) => setGallery(media)}
/>maxItems > 1automatically enables multi-mode. Once the user reaches the cap, additional clicks on unselected cards are silently no-op'd — they have to deselect something to swap.
Controlled mode
If you want the parent to drive open/close (e.g. opening from a context menu), pass open + onOpenChange. The trigger is still rendered so its click also opens the modal — to render only the modal, pass an empty fragment for trigger.
const [open, setOpen] = useState(false)
<MediaManagerTrigger
client={client}
open={open}
onOpenChange={setOpen}
trigger={<></>}
onSelect={(media) => { /* … */ }}
/>Props
| Name | Type | Description |
|---|---|---|
| clientrequired | Sentroy | Same client you pass to MediaManager |
| triggerrequired | ReactNode | The clickable element. Wrapped in <span role="button"> with click + keyboard (Enter / Space) handlers |
| onSelectrequired | (selected: Media[]) => void | Fires when user confirms; modal auto-closes |
| maxItems | number | 1 = single (default), >1 = multi up to cap |
| accept | string | Same <input accept> syntax — applies to upload and grid filter |
| title | string | Modal heading. Default "Select media" |
| description | string | Subheading under the title |
| open | boolean | Controlled open state |
| onOpenChange | (open: boolean) => void | Controlled change handler |
| disabled | boolean | Trigger ignores clicks; visual disabled state |
| confirmLabel | string | Default "Use selection" |
| cancelLabel | string | Default "Cancel" |
| modalClassName | string | Class on the modal panel |
| triggerClassName | string | Class on the trigger wrapper span |
| ...rest | MediaManagerProps | bucketSlug, bucketFilter, showDetailsPane, classNames, etc. forwarded to the inner MediaManager |
Lightbox#
Exported separately so you can use it outside MediaManager (e.g. in a feed view).
import { Lightbox } from "@sentroy-co/client-sdk/react"
const [active, setActive] = useState<Media | null>(null)
return (
<>
{/* …trigger… */}
{active && (
<Lightbox media={active} onClose={() => setActive(null)} />
)}
</>
)Image / video / audio render inline; everything else gets a download button. Esc closes; onPrev / onNext add ←/→ navigation.
Helpers#
Tiny utilities re-exported from the React subpath so you don't have to depend on the core SDK package separately.
import {
cn, // tiny class joiner
formatBytes, // 1234 → "1.21 KB"
detectKind, // image | video | audio | pdf | doc | archive | code | other
matchAccept, // matchAccept(file, "image/*,.pdf") → boolean
KIND_LABELS,
type MediaKind,
} from "@sentroy-co/client-sdk/react"Requirements
- Node.js 18+ (uses native
fetch) - React 18+ (only if you import from
/react) - Tailwind CSS in the host app (only for React components)