Skip to content

Media Library

Media is an open contract. Templatical Cloud implements it, the same way your own backend would.

ts
const editor = await initCloud({ container: '#editor', auth: { url: '/api/token' } });

Nothing to configure — this one is on by default. Cloud supplies the provider, and Browse appears on image fields, video thumbnails and custom-block image fields. The modal ships in the editor; Cloud is the store.

The adapter

MethodCloud
listProject library, search / folder / category / cursor forwarded
createUploads the file into the project
updateFilename and alt
deleteBulk remove
foldersNested folders; Cloud flattens a tree payload into the contract's flat list
replaceReplaces the file in place
importFromUrlPulls a remote URL into the library
checkUsageTemplates that reference the assets
frequentlyUsedRecents for the current user
storageQuota ring; null until plan config has loaded

One library per project, shared by everyone on it. Cloud ignores templateId: the store is project-scoped.

Every method is a function. storage, maxFileSize and mimeTypes are live getters over plan config — they fill in after construction, so a snapshot at setup would pin quota to null for the whole session.

When Cloud's store is in play, consumer maxFileSize / mimeTypes on an events-only object are ignored (Cloud's plan owns those limits). Only onCreated / onUpdated / onDeleted forward.

Bringing your own

You can, and initCloud() accepts this as a full swap, the same way it accepts savedBlocks and testEmail. The key takes the same type as init()'s, plus a third shape unique to this entry point: keep Cloud's library and add your own event handlers.

ts
await initCloud({ container, auth });                        // Cloud's library
await initCloud({ container, auth, media: { onCreated } });  // Cloud's library, plus your events
await initCloud({ container, auth, media: mine });           // your own, on Cloud
await initCloud({ container, auth, media: false });          // off — URL field only
mediaStore
omittedCloud
falseoff
options ({ onCreated })Cloud, plus those handlers
full provideryours, not plan-gated

Cloud tells them apart by list, never by whether the value is an object: something with a working list replaces Cloud's store outright, and anything else — false, an events-only object — keeps Cloud's own store and forwards whatever events it carries onto it.

A provider you supply is not plan-gated — the plan licenses Cloud's storage, not the editor's UI. An events-only object still uses Cloud's store.

A malformed object that has create (or other mutations) but no working list falls through to Cloud's store, with a warning naming the ignored methods.

UI override

onRequestMedia is a host widget (Bynder, Cloudinary, a modal of your own). It is not the store. When both are set, the callback wins and Cloud's modal never opens. It returns { url, alt? } — see Images.

ts
const editor = await initCloud({
  container: '#editor',
  auth: { url: '/api/templatical/token' },
  onRequestMedia: async (context) => {
    const selected = await myMediaPicker.open(context);
    if (!selected) return null;
    return { url: selected.url, alt: selected.alt };
  },
});

Standalone

The same UI, without Confirm, via @templatical/media-library. Pass a provider — Cloud is createCloudMediaProvider from @templatical/core/cloud:

ts
import { init } from '@templatical/media-library';
import { createCloudMediaProvider } from '@templatical/core/cloud';

const mediaLibrary = await init({
  container: '#media-library',
  provider: createCloudMediaProvider(authManager),
  onSelect: (asset) => {
    console.log('Selected:', asset.url);
  },
});

onSelect is optional. accept narrows categories; omit it for every category.

Headless use

For server-side or programmatic media operations, MediaApiClient is exported from @templatical/core/cloud. createCloudMediaProvider maps the response onto MediaProvider.

ts
import { MediaApiClient } from '@templatical/core/cloud';

const client = new MediaApiClient(authManager);

const response = await client.browseMedia({ folderId: 'folder-id', search: 'hero', category: 'images' });
const item = await client.uploadMedia(file, folderId);
await client.deleteMedia(['item-id-1', 'item-id-2']);
const usage = await client.checkMediaUsage(['item-id-1']);