@seshuk/payload-media-preview 1.0.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.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +507 -0
  3. package/dist/components/Cell/Cell.client.d.ts +18 -0
  4. package/dist/components/Cell/Cell.client.js +131 -0
  5. package/dist/components/Cell/Cell.scss +43 -0
  6. package/dist/components/Cell/Cell.server.d.ts +11 -0
  7. package/dist/components/Cell/Cell.server.js +33 -0
  8. package/dist/components/ExternalLinkIcon/ExternalLinkIcon.d.ts +5 -0
  9. package/dist/components/ExternalLinkIcon/ExternalLinkIcon.js +17 -0
  10. package/dist/components/ExternalLinkIcon/ExternalLinkIcon.scss +19 -0
  11. package/dist/components/Field/Field.d.ts +18 -0
  12. package/dist/components/Field/Field.js +107 -0
  13. package/dist/components/Field/Field.scss +14 -0
  14. package/dist/components/MediaPreview.constants.d.ts +4 -0
  15. package/dist/components/MediaPreview.constants.js +28 -0
  16. package/dist/components/MediaPreview.d.ts +10 -0
  17. package/dist/components/MediaPreview.js +57 -0
  18. package/dist/components/MediaPreview.types.d.ts +2 -0
  19. package/dist/components/MediaPreview.types.js +1 -0
  20. package/dist/components/MediaPreview.utils.d.ts +6 -0
  21. package/dist/components/MediaPreview.utils.js +38 -0
  22. package/dist/components/Modal/Modal.constants.d.ts +16 -0
  23. package/dist/components/Modal/Modal.constants.js +16 -0
  24. package/dist/components/Modal/Modal.d.ts +19 -0
  25. package/dist/components/Modal/Modal.js +277 -0
  26. package/dist/components/Modal/Modal.scss +173 -0
  27. package/dist/components/Viewer/AudioViewer.d.ts +3 -0
  28. package/dist/components/Viewer/AudioViewer.js +24 -0
  29. package/dist/components/Viewer/IframeViewer.d.ts +3 -0
  30. package/dist/components/Viewer/IframeViewer.js +14 -0
  31. package/dist/components/Viewer/ImageViewer.d.ts +3 -0
  32. package/dist/components/Viewer/ImageViewer.js +10 -0
  33. package/dist/components/Viewer/VideoViewer.d.ts +3 -0
  34. package/dist/components/Viewer/VideoViewer.js +25 -0
  35. package/dist/components/Viewer/Viewer.d.ts +12 -0
  36. package/dist/components/Viewer/Viewer.js +50 -0
  37. package/dist/components/adapterResolver.d.ts +13 -0
  38. package/dist/components/adapterResolver.js +43 -0
  39. package/dist/exports/client.d.ts +5 -0
  40. package/dist/exports/client.js +5 -0
  41. package/dist/exports/rsc.d.ts +2 -0
  42. package/dist/exports/rsc.js +2 -0
  43. package/dist/field.d.ts +18 -0
  44. package/dist/field.js +36 -0
  45. package/dist/index.d.ts +6 -0
  46. package/dist/index.js +90 -0
  47. package/dist/translations/index.d.ts +8 -0
  48. package/dist/translations/index.js +90 -0
  49. package/dist/translations/locales/ar.d.ts +2 -0
  50. package/dist/translations/locales/ar.js +11 -0
  51. package/dist/translations/locales/az.d.ts +2 -0
  52. package/dist/translations/locales/az.js +11 -0
  53. package/dist/translations/locales/bg.d.ts +2 -0
  54. package/dist/translations/locales/bg.js +11 -0
  55. package/dist/translations/locales/bnBd.d.ts +2 -0
  56. package/dist/translations/locales/bnBd.js +11 -0
  57. package/dist/translations/locales/bnIn.d.ts +2 -0
  58. package/dist/translations/locales/bnIn.js +11 -0
  59. package/dist/translations/locales/ca.d.ts +2 -0
  60. package/dist/translations/locales/ca.js +11 -0
  61. package/dist/translations/locales/cs.d.ts +2 -0
  62. package/dist/translations/locales/cs.js +11 -0
  63. package/dist/translations/locales/da.d.ts +2 -0
  64. package/dist/translations/locales/da.js +11 -0
  65. package/dist/translations/locales/de.d.ts +2 -0
  66. package/dist/translations/locales/de.js +11 -0
  67. package/dist/translations/locales/en.d.ts +2 -0
  68. package/dist/translations/locales/en.js +11 -0
  69. package/dist/translations/locales/es.d.ts +2 -0
  70. package/dist/translations/locales/es.js +11 -0
  71. package/dist/translations/locales/et.d.ts +2 -0
  72. package/dist/translations/locales/et.js +11 -0
  73. package/dist/translations/locales/fa.d.ts +2 -0
  74. package/dist/translations/locales/fa.js +11 -0
  75. package/dist/translations/locales/fr.d.ts +2 -0
  76. package/dist/translations/locales/fr.js +11 -0
  77. package/dist/translations/locales/he.d.ts +2 -0
  78. package/dist/translations/locales/he.js +11 -0
  79. package/dist/translations/locales/hr.d.ts +2 -0
  80. package/dist/translations/locales/hr.js +11 -0
  81. package/dist/translations/locales/hu.d.ts +2 -0
  82. package/dist/translations/locales/hu.js +11 -0
  83. package/dist/translations/locales/hy.d.ts +2 -0
  84. package/dist/translations/locales/hy.js +11 -0
  85. package/dist/translations/locales/id.d.ts +2 -0
  86. package/dist/translations/locales/id.js +11 -0
  87. package/dist/translations/locales/is.d.ts +2 -0
  88. package/dist/translations/locales/is.js +11 -0
  89. package/dist/translations/locales/it.d.ts +2 -0
  90. package/dist/translations/locales/it.js +11 -0
  91. package/dist/translations/locales/ja.d.ts +2 -0
  92. package/dist/translations/locales/ja.js +11 -0
  93. package/dist/translations/locales/ko.d.ts +2 -0
  94. package/dist/translations/locales/ko.js +11 -0
  95. package/dist/translations/locales/lt.d.ts +2 -0
  96. package/dist/translations/locales/lt.js +11 -0
  97. package/dist/translations/locales/lv.d.ts +2 -0
  98. package/dist/translations/locales/lv.js +11 -0
  99. package/dist/translations/locales/my.d.ts +2 -0
  100. package/dist/translations/locales/my.js +11 -0
  101. package/dist/translations/locales/nb.d.ts +2 -0
  102. package/dist/translations/locales/nb.js +11 -0
  103. package/dist/translations/locales/nl.d.ts +2 -0
  104. package/dist/translations/locales/nl.js +11 -0
  105. package/dist/translations/locales/pl.d.ts +2 -0
  106. package/dist/translations/locales/pl.js +11 -0
  107. package/dist/translations/locales/pt.d.ts +2 -0
  108. package/dist/translations/locales/pt.js +11 -0
  109. package/dist/translations/locales/ro.d.ts +2 -0
  110. package/dist/translations/locales/ro.js +11 -0
  111. package/dist/translations/locales/rs.d.ts +2 -0
  112. package/dist/translations/locales/rs.js +11 -0
  113. package/dist/translations/locales/rsLatin.d.ts +2 -0
  114. package/dist/translations/locales/rsLatin.js +11 -0
  115. package/dist/translations/locales/ru.d.ts +2 -0
  116. package/dist/translations/locales/ru.js +11 -0
  117. package/dist/translations/locales/sk.d.ts +2 -0
  118. package/dist/translations/locales/sk.js +11 -0
  119. package/dist/translations/locales/sl.d.ts +2 -0
  120. package/dist/translations/locales/sl.js +11 -0
  121. package/dist/translations/locales/sv.d.ts +2 -0
  122. package/dist/translations/locales/sv.js +11 -0
  123. package/dist/translations/locales/ta.d.ts +2 -0
  124. package/dist/translations/locales/ta.js +11 -0
  125. package/dist/translations/locales/th.d.ts +2 -0
  126. package/dist/translations/locales/th.js +11 -0
  127. package/dist/translations/locales/tr.d.ts +2 -0
  128. package/dist/translations/locales/tr.js +11 -0
  129. package/dist/translations/locales/uk.d.ts +2 -0
  130. package/dist/translations/locales/uk.js +11 -0
  131. package/dist/translations/locales/vi.d.ts +2 -0
  132. package/dist/translations/locales/vi.js +11 -0
  133. package/dist/translations/locales/zh.d.ts +2 -0
  134. package/dist/translations/locales/zh.js +11 -0
  135. package/dist/translations/locales/zhTw.d.ts +2 -0
  136. package/dist/translations/locales/zhTw.js +11 -0
  137. package/dist/translations/types.d.ts +11 -0
  138. package/dist/translations/types.js +1 -0
  139. package/dist/types.d.ts +130 -0
  140. package/dist/types.js +2 -0
  141. package/dist/utils/insertField.d.ts +3 -0
  142. package/dist/utils/insertField.js +97 -0
  143. package/package.json +121 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maxim Seshuk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,507 @@
1
+ # Media Preview Plugin for Payload CMS
2
+
3
+ [![GitHub Release](https://img.shields.io/github/v/release/maximseshuk/payload-plugin-media-preview.svg)](https://github.com/maximseshuk/payload-plugin-media-preview/releases/) [![npm](https://img.shields.io/badge/npm-blue.svg)](https://www.npmjs.com/package/@seshuk/payload-media-preview) [![license](https://img.shields.io/badge/license-grey.svg)](https://github.com/maximseshuk/payload-plugin-media-preview/blob/main/LICENSE) [![NPM Downloads](https://img.shields.io/npm/dm/@seshuk/payload-media-preview)](https://www.npmjs.com/package/@seshuk/payload-media-preview) [![Ko-fi](https://img.shields.io/badge/Ko--fi-Buy_me_a_coffee-ff5f5f?logo=ko-fi)](https://ko-fi.com/V7V61UCT39)
4
+
5
+ Preview images, videos, audio, and documents directly in the Payload CMS admin panel.
6
+
7
+ ## Features
8
+
9
+ - Inline previews in list view cells and edit view fields
10
+ - Popup previews on desktop, fullscreen modals on mobile
11
+ - Built-in viewers for images, video, audio, and documents (PDF, Office, etc.)
12
+ - Extensible adapter system for custom viewers
13
+ - Zero database fields — works as a virtual UI field
14
+
15
+ ## Table of Contents
16
+
17
+ - [Requirements](#requirements)
18
+ - [Installation](#installation)
19
+ - [Quick Start](#quick-start)
20
+ - [Configuration](#configuration)
21
+ - [Plugin Config](#plugin-config)
22
+ - [Collection Config](#collection-config)
23
+ - [Display Modes](#display-modes)
24
+ - [Content Modes](#content-modes)
25
+ - [Field Position](#field-position)
26
+ - [Supported File Types](#supported-file-types)
27
+ - [Adapters](#adapters)
28
+ - [Standalone Field](#standalone-field)
29
+ - [Internationalization](#internationalization)
30
+ - [Exports](#exports)
31
+ - [TypeScript](#typescript)
32
+ - [License](#license)
33
+
34
+ ## Requirements
35
+
36
+ - Payload `^3.53.0`
37
+ - Node.js `^18.20.2 || >=20.9.0`
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pnpm add @seshuk/payload-media-preview
43
+ # or
44
+ npm install @seshuk/payload-media-preview
45
+ # or
46
+ yarn add @seshuk/payload-media-preview
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ```ts
52
+ import { mediaPreview } from '@seshuk/payload-media-preview'
53
+ import { buildConfig } from 'payload'
54
+
55
+ export default buildConfig({
56
+ // ...
57
+ plugins: [
58
+ mediaPreview({
59
+ collections: {
60
+ media: true,
61
+ },
62
+ }),
63
+ ],
64
+ })
65
+ ```
66
+
67
+ This adds a preview column to your `media` collection's list view and a preview button to the edit view.
68
+
69
+ ## Configuration
70
+
71
+ ### Plugin Config
72
+
73
+ | Option | Type | Default | Description |
74
+ | ------------- | ------------------------------------------ | ------- | -------------------------------------------- |
75
+ | `enabled` | `boolean` | `true` | Enable or disable the plugin |
76
+ | `adapters` | `MediaPreviewAdapter[]` | `[]` | Global adapters available to all collections |
77
+ | `collections` | `Record<string, CollectionConfig \| true>` | — | Which upload collections to add preview to |
78
+
79
+ ### Collection Config
80
+
81
+ Each collection entry can be `true` (all defaults) or an object:
82
+
83
+ | Option | Type | Default | Description |
84
+ | ------------- | ------------------------------------ | ---------- | ----------------------------------------------------------------- |
85
+ | `mode` | `'auto' \| 'fullscreen'` | `'auto'` | Preview display mode |
86
+ | `contentMode` | `Partial<MediaPreviewContentMode>` | all inline | How to open each content type (`'inline'` or `'newTab'`) |
87
+ | `adapters` | `MediaPreviewAdapter[]` | — | Per-collection adapters (override global) |
88
+ | `field` | `false \| { position?, overrides? }` | `{}` | Field injection config, or `false` to skip (for manual placement) |
89
+
90
+ **`field` options:**
91
+
92
+ | Option | Type | Default | Description |
93
+ | ----------- | ------------------------------------------ | -------- | ---------------------------------------------------------------- |
94
+ | `position` | `'first' \| 'last' \| { after \| before }` | `'last'` | Where to insert the preview field |
95
+ | `overrides` | `Partial<Omit<UIField, 'name' \| 'type'>>` | — | Payload UI field overrides (`name` and `type` cannot be changed) |
96
+
97
+ ```ts
98
+ mediaPreview({
99
+ collections: {
100
+ media: true,
101
+ 'hero-images': {
102
+ mode: 'fullscreen',
103
+ contentMode: { video: 'newTab' },
104
+ field: { position: { after: 'alt' } },
105
+ },
106
+ },
107
+ })
108
+ ```
109
+
110
+ ### Display Modes
111
+
112
+ The `mode` option controls how previews are displayed:
113
+
114
+ #### `'auto'` (default)
115
+
116
+ Smart mode that adapts to context and device:
117
+
118
+ | Context | Desktop | Mobile |
119
+ | ----------------- | ---------------------------- | ---------------- |
120
+ | Cell (list view) | Floating popup near the cell | Fullscreen modal |
121
+ | Field (edit view) | Fullscreen modal | Fullscreen modal |
122
+
123
+ #### `'fullscreen'`
124
+
125
+ Always uses a fullscreen modal, regardless of context or device.
126
+
127
+ ```ts
128
+ collections: {
129
+ media: {
130
+ mode: 'fullscreen',
131
+ },
132
+ }
133
+ ```
134
+
135
+ ### Content Modes
136
+
137
+ Control how each content type is opened with the `contentMode` option. Each content type can be set to `'inline'` (default) or `'newTab'`:
138
+
139
+ | Mode | Behavior |
140
+ | ---------- | --------------------------------- |
141
+ | `'inline'` | Show content in modal preview |
142
+ | `'newTab'` | Open content in a new browser tab |
143
+
144
+ ```ts
145
+ collections: {
146
+ media: {
147
+ contentMode: {
148
+ video: 'newTab', // open videos in a new tab
149
+ document: 'newTab', // open documents in a new tab
150
+ image: 'inline', // show images in modal (default)
151
+ audio: 'inline', // show audio in modal (default)
152
+ },
153
+ },
154
+ }
155
+ ```
156
+
157
+ ### Field Position
158
+
159
+ Control where the preview field appears in the edit view with the `field.position` option:
160
+
161
+ ```ts
162
+ // At the end (default)
163
+ field: {
164
+ position: 'last'
165
+ }
166
+
167
+ // At the beginning
168
+ field: {
169
+ position: 'first'
170
+ }
171
+
172
+ // After a specific field
173
+ field: {
174
+ position: {
175
+ after: 'alt'
176
+ }
177
+ }
178
+
179
+ // Before a specific field
180
+ field: {
181
+ position: {
182
+ before: 'description'
183
+ }
184
+ }
185
+ ```
186
+
187
+ Dot-notation paths are supported for fields inside named tabs and nested groups:
188
+
189
+ ```ts
190
+ // After a field inside a tab
191
+ field: {
192
+ position: {
193
+ after: 'myTab.fieldName'
194
+ }
195
+ }
196
+ ```
197
+
198
+ ### Supported File Types
199
+
200
+ #### Images
201
+
202
+ All `image/*` MIME types — displayed using the native `<img>` element.
203
+
204
+ #### Video
205
+
206
+ All `video/*` MIME types — displayed using the native `<video>` element with controls.
207
+
208
+ #### Audio
209
+
210
+ All `audio/*` MIME types — displayed using the native `<audio>` element with controls.
211
+
212
+ #### Documents
213
+
214
+ Documents are previewed via external viewer services:
215
+
216
+ **Microsoft Office Online Viewer** — for Office formats:
217
+
218
+ - `.doc`, `.docx` (Word)
219
+ - `.xls`, `.xlsx` (Excel)
220
+ - `.ppt`, `.pptx` (PowerPoint)
221
+
222
+ **Google Docs Viewer** — for other document types:
223
+
224
+ - `.pdf`
225
+ - `.txt`, `.css`, `.html`, `.js`, `.php`, `.c`, `.cpp`
226
+ - `.pages` (Apple Pages)
227
+ - `.ai`, `.eps`, `.ps` (PostScript)
228
+ - `.psd` (Photoshop)
229
+ - `.dxf` (AutoCAD)
230
+ - `.svg`
231
+ - `.xps`
232
+
233
+ > **Note:** Document previews use external services (Google Docs Viewer, Microsoft Office Online) that fetch files by URL. This only works when your media URLs are publicly accessible. Google Docs Viewer also has a 25 MB file size limit.
234
+
235
+ ## Adapters
236
+
237
+ Adapters let you customize how files are previewed. When an adapter matches a document, it takes priority over built-in viewers and `contentMode` settings.
238
+
239
+ Each adapter has a `resolve()` function that returns one of two modes:
240
+
241
+ - `{ mode: 'inline', props }` — renders a custom component inside the modal preview
242
+ - `{ mode: 'newTab', url }` — opens a link in a new browser tab
243
+
244
+ The `Component` field is only needed for `inline` mode. Adapters that only use `newTab` mode don't need a component.
245
+
246
+ ### Examples
247
+
248
+ ```ts
249
+ import type { IframeViewerProps, MediaPreviewAdapter } from '@seshuk/payload-media-preview'
250
+
251
+ // Inline — custom component in modal
252
+ const videoAdapter: MediaPreviewAdapter = {
253
+ name: 'video-embed',
254
+ Component: 'my-pkg/client#VideoPlayer',
255
+ resolve: ({ doc }) => {
256
+ if (!doc.videoId) return null
257
+ return {
258
+ mode: 'inline',
259
+ props: { videoId: doc.videoId, autoplay: false },
260
+ }
261
+ },
262
+ }
263
+
264
+ // NewTab — opens link in new browser tab (no Component needed)
265
+ const externalPreview: MediaPreviewAdapter = {
266
+ name: 'external',
267
+ resolve: ({ url }) => {
268
+ if (!url) return null
269
+ return {
270
+ mode: 'newTab',
271
+ url: `https://preview.service.com/?file=${encodeURIComponent(url)}`,
272
+ }
273
+ },
274
+ }
275
+
276
+ // Built-in viewer with typed props via satisfies
277
+ const iframeAdapter: MediaPreviewAdapter = {
278
+ name: 'iframe',
279
+ Component: '@seshuk/payload-media-preview/client#IframeViewer',
280
+ resolve: ({ url }) => {
281
+ if (!url) return null
282
+ return {
283
+ mode: 'inline',
284
+ props: { src: url, allowFullScreen: true } satisfies IframeViewerProps,
285
+ }
286
+ },
287
+ }
288
+ ```
289
+
290
+ ### Registering Adapters
291
+
292
+ ```ts
293
+ mediaPreview({
294
+ adapters: [videoAdapter], // global adapters
295
+ collections: {
296
+ media: {
297
+ adapters: [externalPreview], // per-collection adapters
298
+ },
299
+ },
300
+ })
301
+ ```
302
+
303
+ ### How Adapters Work
304
+
305
+ 1. When a document is loaded, all registered adapters are tried in order
306
+ 2. Each adapter's `resolve()` function receives `{ doc, url, mimeType }`
307
+ 3. The first adapter to return a non-null value wins
308
+ 4. For `inline` results, the `props` are passed to the adapter's `Component`
309
+ 5. For `newTab` results, clicking the preview opens the URL in a new browser tab
310
+ 6. If no adapter matches, the default built-in viewer is used
311
+
312
+ ### Built-in Viewer Components
313
+
314
+ The plugin exports four built-in viewer components that you can use in adapters with `inline` mode:
315
+
316
+ | Component | Import Path | Props Type |
317
+ | -------------- | --------------------------------------------------- | ------------------- |
318
+ | `ImageViewer` | `@seshuk/payload-media-preview/client#ImageViewer` | `ImageViewerProps` |
319
+ | `VideoViewer` | `@seshuk/payload-media-preview/client#VideoViewer` | `VideoViewerProps` |
320
+ | `AudioViewer` | `@seshuk/payload-media-preview/client#AudioViewer` | `AudioViewerProps` |
321
+ | `IframeViewer` | `@seshuk/payload-media-preview/client#IframeViewer` | `IframeViewerProps` |
322
+
323
+ ### Adapter Props Reference
324
+
325
+ `**ImageViewerProps**`
326
+
327
+ | Prop | Type | Required |
328
+ | ----------- | -------- | -------- |
329
+ | `src` | `string` | Yes |
330
+ | `alt` | `string` | No |
331
+ | `className` | `string` | No |
332
+
333
+ **`VideoViewerProps`**
334
+
335
+ | Prop | Type | Required |
336
+ | ----------- | -------------------------------- | -------- |
337
+ | `src` | `string` | Yes |
338
+ | `mimeType` | `string` | No |
339
+ | `title` | `string` | No |
340
+ | `controls` | `boolean` | No |
341
+ | `autoPlay` | `boolean` | No |
342
+ | `loop` | `boolean` | No |
343
+ | `muted` | `boolean` | No |
344
+ | `preload` | `'auto' \| 'metadata' \| 'none'` | No |
345
+ | `className` | `string` | No |
346
+
347
+ **`AudioViewerProps`**
348
+
349
+ | Prop | Type | Required |
350
+ | ----------- | -------------------------------- | -------- |
351
+ | `src` | `string` | Yes |
352
+ | `mimeType` | `string` | No |
353
+ | `title` | `string` | No |
354
+ | `controls` | `boolean` | No |
355
+ | `autoPlay` | `boolean` | No |
356
+ | `loop` | `boolean` | No |
357
+ | `muted` | `boolean` | No |
358
+ | `preload` | `'auto' \| 'metadata' \| 'none'` | No |
359
+ | `className` | `string` | No |
360
+
361
+ **`IframeViewerProps`**
362
+
363
+ | Prop | Type | Required |
364
+ | ----------------- | ------------------- | -------- |
365
+ | `src` | `string` | Yes |
366
+ | `title` | `string` | No |
367
+ | `allow` | `string` | No |
368
+ | `allowFullScreen` | `boolean` | No |
369
+ | `loading` | `'eager' \| 'lazy'` | No |
370
+ | `className` | `string` | No |
371
+
372
+ ## Standalone Field
373
+
374
+ The plugin automatically injects the preview field into configured collections. If you need more control over field placement, you can add the field manually using `mediaPreviewField()`.
375
+
376
+ ### With `field: false`
377
+
378
+ Use `field: false` in the collection config to register adapters without injecting the field. This lets you place the field manually while keeping all adapter and translation registration:
379
+
380
+ ```ts
381
+ import type { MediaPreviewAdapter } from '@seshuk/payload-media-preview'
382
+ import { mediaPreview, mediaPreviewField } from '@seshuk/payload-media-preview'
383
+
384
+ const videoAdapter: MediaPreviewAdapter = {
385
+ name: 'video-embed',
386
+ Component: 'my-pkg/client#VideoPlayer',
387
+ resolve: ({ doc }) => {
388
+ if (!doc.videoId) return null
389
+ return { mode: 'inline', props: { videoId: doc.videoId } }
390
+ },
391
+ }
392
+
393
+ export default buildConfig({
394
+ collections: [
395
+ {
396
+ slug: 'media',
397
+ upload: true,
398
+ fields: [
399
+ { name: 'alt', type: 'text' },
400
+ mediaPreviewField({
401
+ adapterNames: ['video-embed'],
402
+ mode: 'fullscreen',
403
+ }),
404
+ { name: 'caption', type: 'textarea' },
405
+ ],
406
+ },
407
+ ],
408
+ plugins: [
409
+ mediaPreview({
410
+ collections: {
411
+ media: {
412
+ adapters: [videoAdapter],
413
+ field: false, // don't inject — already added manually above
414
+ },
415
+ },
416
+ }),
417
+ ],
418
+ })
419
+ ```
420
+
421
+ ### Without collection registration
422
+
423
+ If you don't need per-collection adapters, you can omit the collection from the plugin config entirely and use global adapters:
424
+
425
+ ```ts
426
+ import { mediaPreview, mediaPreviewField } from '@seshuk/payload-media-preview'
427
+
428
+ export default buildConfig({
429
+ collections: [
430
+ {
431
+ slug: 'media',
432
+ upload: true,
433
+ fields: [{ name: 'alt', type: 'text' }, mediaPreviewField({ mode: 'fullscreen' })],
434
+ },
435
+ ],
436
+ plugins: [
437
+ mediaPreview({
438
+ collections: {},
439
+ }),
440
+ ],
441
+ })
442
+ ```
443
+
444
+ The plugin must still be included to register viewer components and translations. Pass `adapterNames` to `mediaPreviewField()` to use adapters — those adapters must be registered via the plugin's `adapters` (global) or collection `adapters` config.
445
+
446
+ ## Internationalization
447
+
448
+ The plugin includes translations for 44 locales. Translations are automatically merged into your Payload i18n configuration under the `@seshuk/payload-media-preview` namespace.
449
+
450
+ Supported locales: `ar`, `az`, `bg`, `bn` (BD/IN), `ca`, `cs`, `da`, `de`, `en`, `es`, `et`, `fa`, `fi`, `fr`, `he`, `hr`, `hu`, `hy`, `id`, `is`, `it`, `ja`, `ka`, `ko`, `lt`, `lv`, `mk`, `nb`, `nl`, `pl`, `pt`, `ro`, `rs` (Cyrillic/Latin), `ru`, `sk`, `sl`, `sv`, `th`, `tr`, `uk`, `vi`, `zh`, `zhTw`.
451
+
452
+ ## Exports
453
+
454
+ The package provides three entry points:
455
+
456
+ | Entry Point | Description | Usage |
457
+ | -------------------------------------- | -------------------------------------------------- | ------------------------- |
458
+ | `@seshuk/payload-media-preview` | Plugin function and all public types | Server-side config |
459
+ | `@seshuk/payload-media-preview/client` | Client components (Field, Cell, Modal, Viewers) | `'use client'` components |
460
+ | `@seshuk/payload-media-preview/rsc` | Server components (MediaPreview, MediaPreviewCell) | React Server Components |
461
+
462
+ ## TypeScript
463
+
464
+ All types are exported from the main entry point:
465
+
466
+ ```ts
467
+ import type {
468
+ MediaPreviewAdapter,
469
+ MediaPreviewAdapterResolveArgs,
470
+ MediaPreviewAdapterInlineResult,
471
+ MediaPreviewAdapterNewTabResult,
472
+ MediaPreviewAdapterResolveResult,
473
+ MediaPreviewCollectionConfig,
474
+ MediaPreviewContentMode,
475
+ MediaPreviewContentModeType,
476
+ MediaPreviewContentType,
477
+ MediaPreviewFieldConfig,
478
+ MediaPreviewMode,
479
+ MediaPreviewPluginConfig,
480
+ InsertPosition,
481
+ ImageViewerProps,
482
+ VideoViewerProps,
483
+ AudioViewerProps,
484
+ IframeViewerProps,
485
+ } from '@seshuk/payload-media-preview'
486
+ ```
487
+
488
+ ---
489
+
490
+ ## License
491
+
492
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
493
+
494
+ ## Related Plugins
495
+
496
+ - **[@seshuk/payload-storage-bunny](https://github.com/maximseshuk/payload-storage-bunny)** — Bunny.net storage adapter for Payload CMS
497
+
498
+ ## Support
499
+
500
+ - **Bug Reports**: [GitHub Issues](https://github.com/maximseshuk/payload-plugin-media-preview/issues)
501
+ - **Questions**: Join the payload-plugin-media-preview in [GitHub Issues](https://github.com/maximseshuk/payload-plugin-media-preview/issues) or [Payload CMS Discord](https://discord.gg/payloadcms)
502
+
503
+ ## Credits
504
+
505
+ Built with ❤️ for the Payload CMS community.
506
+
507
+ If you find this plugin useful, [buy me a coffee](https://ko-fi.com/V7V61UCT39).
@@ -0,0 +1,18 @@
1
+ import type { MediaPreviewContentMode, MediaPreviewMode } from '@/types.js';
2
+ import React from 'react';
3
+ type Props = {
4
+ adapterNewTabUrl?: string;
5
+ contentMode?: Partial<MediaPreviewContentMode>;
6
+ customViewer?: React.ReactNode;
7
+ media: {
8
+ fileSize?: number;
9
+ height?: number;
10
+ mimeType?: string;
11
+ url?: string;
12
+ width?: number;
13
+ };
14
+ mode?: MediaPreviewMode;
15
+ rowId?: number | string;
16
+ };
17
+ export declare const MediaPreviewCellClient: React.FC<Props>;
18
+ export {};
@@ -0,0 +1,131 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Pill, useTranslation } from "@payloadcms/ui";
4
+ import { EyeIcon } from "@payloadcms/ui/icons/Eye";
5
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
+ import { ExternalLinkIcon } from "../ExternalLinkIcon/ExternalLinkIcon.js";
7
+ import { canPreviewDocument, getDocumentViewerType, getGoogleViewerUrl, getMicrosoftViewerUrl, getPreviewType } from "../MediaPreview.utils.js";
8
+ import { MediaPreviewModal } from "../Modal/Modal.js";
9
+ export const MediaPreviewCellClient = ({ adapterNewTabUrl, contentMode, customViewer, media, mode = 'auto', rowId })=>{
10
+ const { fileSize, height, mimeType, url, width } = media;
11
+ const [isTouchDevice, setIsTouchDevice] = useState(false);
12
+ const [showModal, setShowModal] = useState(false);
13
+ const buttonRef = useRef(null);
14
+ const { t } = useTranslation();
15
+ const audioViewerMode = contentMode?.audio ?? 'inline';
16
+ const documentViewerMode = contentMode?.document ?? 'inline';
17
+ const imageViewerMode = contentMode?.image ?? 'inline';
18
+ const videoViewerMode = contentMode?.video ?? 'inline';
19
+ const previewType = useMemo(()=>getPreviewType(mimeType), [
20
+ mimeType
21
+ ]);
22
+ const isAudioFile = previewType === 'audio';
23
+ const isDocumentFile = previewType === 'document';
24
+ const isImageFile = previewType === 'image';
25
+ const isVideoFile = previewType === 'video';
26
+ const canPreview = !isDocumentFile || canPreviewDocument(fileSize);
27
+ const modalMode = useMemo(()=>mode === 'fullscreen' ? 'fullscreen' : isTouchDevice ? 'fullscreen' : 'popup', [
28
+ mode,
29
+ isTouchDevice
30
+ ]);
31
+ const documentViewerUrl = useMemo(()=>{
32
+ if (isDocumentFile && url && mimeType) {
33
+ const viewerType = getDocumentViewerType(mimeType);
34
+ return viewerType === 'microsoft' ? getMicrosoftViewerUrl(url) : getGoogleViewerUrl(url);
35
+ }
36
+ return null;
37
+ }, [
38
+ isDocumentFile,
39
+ url,
40
+ mimeType
41
+ ]);
42
+ useEffect(()=>{
43
+ setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0);
44
+ }, []);
45
+ const handleClick = useCallback((e)=>{
46
+ e.stopPropagation();
47
+ setShowModal((prev)=>!prev);
48
+ }, []);
49
+ const renderNewTabLink = useCallback((href)=>/*#__PURE__*/ _jsx("div", {
50
+ className: "media-preview-cell",
51
+ children: /*#__PURE__*/ _jsx("a", {
52
+ className: "media-preview-cell__button-wrapper",
53
+ href: href,
54
+ rel: "noopener noreferrer",
55
+ target: "_blank",
56
+ children: /*#__PURE__*/ _jsx(Pill, {
57
+ alignIcon: "left",
58
+ className: "media-preview-cell__pill",
59
+ icon: /*#__PURE__*/ _jsx(ExternalLinkIcon, {
60
+ className: "media-preview-cell__icon"
61
+ }),
62
+ size: "small",
63
+ children: t('@seshuk/payload-media-preview:open')
64
+ })
65
+ })
66
+ }), [
67
+ t
68
+ ]);
69
+ if (previewType === 'unsupported' || !url || !canPreview) {
70
+ if (!customViewer) {
71
+ return /*#__PURE__*/ _jsx("span", {
72
+ children: "—"
73
+ });
74
+ }
75
+ }
76
+ if (adapterNewTabUrl) {
77
+ return renderNewTabLink(adapterNewTabUrl);
78
+ }
79
+ if (!customViewer) {
80
+ if (isVideoFile && videoViewerMode === 'newTab' && url) {
81
+ return renderNewTabLink(url);
82
+ }
83
+ if (isAudioFile && audioViewerMode === 'newTab' && url) {
84
+ return renderNewTabLink(url);
85
+ }
86
+ if (isImageFile && imageViewerMode === 'newTab' && url) {
87
+ return renderNewTabLink(url);
88
+ }
89
+ if (isDocumentFile && documentViewerMode === 'newTab' && documentViewerUrl) {
90
+ return renderNewTabLink(documentViewerUrl);
91
+ }
92
+ }
93
+ return /*#__PURE__*/ _jsxs(_Fragment, {
94
+ children: [
95
+ /*#__PURE__*/ _jsx("div", {
96
+ className: "media-preview-cell",
97
+ children: /*#__PURE__*/ _jsx("button", {
98
+ className: "media-preview-cell__button-wrapper",
99
+ onClick: handleClick,
100
+ ref: buttonRef,
101
+ type: "button",
102
+ children: /*#__PURE__*/ _jsx(Pill, {
103
+ alignIcon: "left",
104
+ className: `media-preview-cell__pill ${showModal ? 'media-preview-cell__pill--active' : ''}`,
105
+ icon: /*#__PURE__*/ _jsx(EyeIcon, {
106
+ active: modalMode === 'popup' && showModal,
107
+ className: "media-preview-cell__icon"
108
+ }),
109
+ size: "small",
110
+ children: modalMode === 'popup' && showModal ? t('@seshuk/payload-media-preview:close') : t('@seshuk/payload-media-preview:open')
111
+ })
112
+ })
113
+ }),
114
+ /*#__PURE__*/ _jsx(MediaPreviewModal, {
115
+ customViewer: customViewer,
116
+ media: {
117
+ documentViewerUrl,
118
+ height,
119
+ mimeType,
120
+ url,
121
+ width
122
+ },
123
+ mode: modalMode,
124
+ onClose: ()=>setShowModal(false),
125
+ rowId: rowId,
126
+ show: showModal,
127
+ triggerRef: buttonRef
128
+ })
129
+ ]
130
+ });
131
+ };