@pantheon-systems/p1-media 0.4.0 → 0.4.1
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/package.json +1 -1
- package/README.md +0 -322
package/package.json
CHANGED
package/README.md
DELETED
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
# @pantheon-systems/p1-media
|
|
2
|
-
|
|
3
|
-
A Puck editor plugin that adds a media library, a rich `p1-media` field type, and render
|
|
4
|
-
helpers for Puck-based P1 sites. Backed by a Cloudflare Worker + R2 + D1 media store — see
|
|
5
|
-
the [monorepo README](https://github.com/pantheon-systems/p1-media-r2/blob/main/README.md) for the backend and the
|
|
6
|
-
[API reference](https://github.com/pantheon-systems/p1-media-r2/blob/main/docs/openapi.yaml) for calling the Worker directly.
|
|
7
|
-
|
|
8
|
-
The plugin has two distinct audiences with different levels of involvement.
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## For component developers
|
|
13
|
-
|
|
14
|
-
If you are building a Puck component for a P1 site, you do not need to install or configure the plugin. CCR wires it up automatically when it initialises the Puck editor — `workerUrl`, `siteId`, `workstreamId`, and `getAuthToken` all come from CCR context.
|
|
15
|
-
|
|
16
|
-
There are two ways to author an image field:
|
|
17
|
-
|
|
18
|
-
- **Basic mode** — name a text field with a standard pattern (below); the picker activates automatically and stores a plain CDN URL string. Render it with `buildImageUrl`.
|
|
19
|
-
- **Rich mode** — declare a `p1-media` field; it stores a `MediaValue` object carrying the version URL plus metadata (alt, caption, …). Render it with the `getMediaProps` / `MediaImage` / `MediaFigure` helpers.
|
|
20
|
-
|
|
21
|
-
Both are supported; basic mode is preserved indefinitely.
|
|
22
|
-
|
|
23
|
-
### Field naming — auto-detected patterns
|
|
24
|
-
|
|
25
|
-
Name your image fields using any of the following patterns and the media library picker will appear automatically in the Puck sidebar:
|
|
26
|
-
|
|
27
|
-
- `image`, `imageUrl`
|
|
28
|
-
- `logo`, `logoUrl`
|
|
29
|
-
- `media`, `mediaUrl`
|
|
30
|
-
- `icon`, `iconUrl`
|
|
31
|
-
- `thumbnail`, `thumbnailUrl`
|
|
32
|
-
- Any field ending in `ImageUrl` or `LogoUrl`
|
|
33
|
-
|
|
34
|
-
Navigation URL patterns (`buttonUrl`, `linkUrl`, `ctaUrl`) and alt text fields are excluded.
|
|
35
|
-
|
|
36
|
-
### Declaring a rich media field (`p1-media`)
|
|
37
|
-
|
|
38
|
-
The quickest integration is the ready-made component the plugin ships:
|
|
39
|
-
|
|
40
|
-
```tsx
|
|
41
|
-
import { createMediaFigureBlock } from "@pantheon-systems/p1-media";
|
|
42
|
-
|
|
43
|
-
const config = {
|
|
44
|
-
components: {
|
|
45
|
-
MediaFigureBlock: createMediaFigureBlock({
|
|
46
|
-
mediaBaseUrl: "https://media.p1.pantheon.io",
|
|
47
|
-
}),
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
`createMediaFigureBlock` options:
|
|
53
|
-
|
|
54
|
-
| Option | Default | Description |
|
|
55
|
-
|--------|---------|-------------|
|
|
56
|
-
| `mediaBaseUrl` | `"https://media.p1.pantheon.io"` (production) | CDN image origin used to validate value URLs — NOT the Worker API URL. Override for sandbox/staging/local dev |
|
|
57
|
-
| `transform` | `{ width: 1200, height: 630, format: "auto" }` | Render-time transform; keep both dimensions (see crop note below) |
|
|
58
|
-
| `label` | `"Media Figure"` | Component label in the Puck sidebar |
|
|
59
|
-
| `fieldLabel` | `"Photo"` | Label of the media field |
|
|
60
|
-
| `schema` | — | `MetadataFieldDef[]` passed to `MediaFigure`; pins figcaption field order/labels |
|
|
61
|
-
| `className` / `captionClassName` | — | Forwarded to `MediaFigure` |
|
|
62
|
-
| `placeholder` | `"Choose a photo from the media library"` | Rendered when no photo is chosen (or its URL fails validation) |
|
|
63
|
-
|
|
64
|
-
To put the field on your own component instead, declare it directly. `p1-media` is
|
|
65
|
-
registered by the plugin at editor runtime, so it is not part of Puck's built-in `Field`
|
|
66
|
-
union — cast the declaration:
|
|
67
|
-
|
|
68
|
-
```tsx
|
|
69
|
-
import type { Field } from "@puckeditor/core";
|
|
70
|
-
import { MediaImage, type MediaFieldValue } from "@pantheon-systems/p1-media";
|
|
71
|
-
|
|
72
|
-
const heroBlock = {
|
|
73
|
-
fields: {
|
|
74
|
-
photo: { type: "p1-media", label: "Photo" } as unknown as Field,
|
|
75
|
-
},
|
|
76
|
-
defaultProps: { photo: null },
|
|
77
|
-
render: ({ photo }: { photo?: MediaFieldValue | null }) => (
|
|
78
|
-
<MediaImage image={photo} mediaBaseUrl={MEDIA_BASE} transform={{ width: 1200, height: 630 }} />
|
|
79
|
-
),
|
|
80
|
-
};
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Uploading with metadata
|
|
84
|
-
|
|
85
|
-
Choosing or dropping files in the library modal stages them in a metadata grid before
|
|
86
|
-
anything is uploaded: one row per image, one column per schema field (alt, caption, …).
|
|
87
|
-
Filling it is optional — Upload sends whatever is non-empty as metadata defaults on each
|
|
88
|
-
asset. Every item's ✎ button opens the same grid to edit an existing asset's metadata
|
|
89
|
-
(empty cells clear the field via `PATCH /media/:assetId`).
|
|
90
|
-
|
|
91
|
-
The grid is keyboard-first: Tab moves through cells, Enter/↓ and ↑ move within a column.
|
|
92
|
-
Pasting multi-cell TSV or CSV (e.g. copied from a spreadsheet) fills the grid from the
|
|
93
|
-
focused cell. If the first pasted row is a header naming schema fields (`alt`, `caption`,
|
|
94
|
-
…), columns are mapped by name instead — and with a `filename` column, rows are matched to
|
|
95
|
-
the staged files by filename, so a whole exported sheet can be pasted into any cell.
|
|
96
|
-
|
|
97
|
-
The edit panel also has **Replace image…**, which uploads a new file as a new immutable
|
|
98
|
-
version of the same asset (`POST /media/:assetId/versions`) — placements pinned to older
|
|
99
|
-
versions keep serving; metadata is untouched.
|
|
100
|
-
|
|
101
|
-
The library grid itself is keyboard-navigable (roving tab stop: arrow keys move between
|
|
102
|
-
tiles, Enter/Space selects, E edits, Delete removes) and lazily rendered — tiles are
|
|
103
|
-
revealed in batches of 60 as you scroll, images load with `loading="lazy"`, and the list
|
|
104
|
-
fetches up to the Worker's 500-item cap (use search beyond that).
|
|
105
|
-
|
|
106
|
-
### Cropping
|
|
107
|
-
|
|
108
|
-
Crop intent always lives in the value's URL query — the stored value stays a plain CDN URL
|
|
109
|
-
(basic mode) or a `MediaValue` whose `url` carries the params (rich mode):
|
|
110
|
-
|
|
111
|
-
| Editor control | URL params | Notes |
|
|
112
|
-
|----------------|------------|-------|
|
|
113
|
-
| Fit in | `?fit=scale-down` | Default; never crops |
|
|
114
|
-
| Smart crop | `?fit=cover&gravity=auto` | Content-aware crop toward the render transform's aspect ratio |
|
|
115
|
-
| Custom… (rich field only) | `?trim.left=…&trim.top=…&trim.width=…&trim.height=…` | Interactive cropper with fixed (1:1, 4:3, 3:2, 16:9) or free aspect ratios; values are source-image pixels |
|
|
116
|
-
|
|
117
|
-
The render-time `transform` composes on top of the crop: a `trim` region is cut from the
|
|
118
|
-
source first, then resized to `width`/`height`. See the
|
|
119
|
-
[image transformation params](https://github.com/pantheon-systems/p1-media-r2/blob/main/docs/openapi.yaml)
|
|
120
|
-
in the API reference for everything the `/image` route accepts.
|
|
121
|
-
|
|
122
|
-
### Rendering — basic mode (string value)
|
|
123
|
-
|
|
124
|
-
The stored value is a clean CDN URL. Use `buildImageUrl` to add size, format, and quality at render time; it preserves the editor's crop intent (`?fit=…&gravity=…`).
|
|
125
|
-
|
|
126
|
-
```tsx
|
|
127
|
-
import { buildImageUrl } from "@pantheon-systems/p1-media";
|
|
128
|
-
|
|
129
|
-
<img src={buildImageUrl(data.heroImage, { width: 1200, height: 630, format: "webp" })} />
|
|
130
|
-
<img src={buildImageUrl(data.thumbnail, { width: 150, height: 150, format: "webp", quality: 80 })} />
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Rendering — rich mode (`p1-media` value)
|
|
134
|
-
|
|
135
|
-
The stored value is a `MediaValue` object. Use the render helpers, which return/apply the
|
|
136
|
-
alt text and captured dimensions. **`mediaBaseUrl`** (the CDN image origin) is required and
|
|
137
|
-
passed at render — the helpers run in RSC and can't read plugin config, and they fail
|
|
138
|
-
closed (empty `src`) for any URL not on that origin. URLs must be `https`, with one
|
|
139
|
-
local-dev exception: when the configured base is itself `http` on a loopback host
|
|
140
|
-
(`http://localhost:8788` — a local `wrangler dev` worker), same-origin `http` URLs are
|
|
141
|
-
allowed so rich values render locally. Production is unaffected (real CDN bases are `https`).
|
|
142
|
-
|
|
143
|
-
```tsx
|
|
144
|
-
import { MediaImage, MediaFigure, getMediaProps } from "@pantheon-systems/p1-media";
|
|
145
|
-
|
|
146
|
-
const MEDIA_BASE = "https://media.p1.pantheon.io";
|
|
147
|
-
|
|
148
|
-
// <img>-like wrapper: src + alt + width/height come from the value
|
|
149
|
-
<MediaImage image={data.heroImage} mediaBaseUrl={MEDIA_BASE} transform={{ width: 1200, height: 630, format: "webp" }} />
|
|
150
|
-
|
|
151
|
-
// figure + caption/credit/byline composed generically from the metadata schema
|
|
152
|
-
<MediaFigure image={data.heroImage} mediaBaseUrl={MEDIA_BASE} transform={{ width: 1200, height: 630, format: "webp" }} />
|
|
153
|
-
|
|
154
|
-
// or spread the raw props onto your own element / next/image
|
|
155
|
-
const { src, alt, width, height } = getMediaProps(data.heroImage, { mediaBaseUrl: MEDIA_BASE, transform: { width: 1200, height: 630 } });
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
> Pass **both** `width` and `height` in the transform. The editor's crop toggle is carried
|
|
159
|
-
> as `?fit=cover&gravity=auto` (smart crop) vs `?fit=scale-down` (fit in), and `fit` only
|
|
160
|
-
> changes the output when there is a target aspect ratio to crop against — with a
|
|
161
|
-
> width-only transform both modes produce identical pixels and the crop control appears
|
|
162
|
-
> to do nothing.
|
|
163
|
-
|
|
164
|
-
All three helpers accept `string | MediaValue | null | undefined` and render nothing
|
|
165
|
-
(`MediaImage`/`MediaFigure` return `null`; `getMediaProps` returns `src: ""`) when the
|
|
166
|
-
value is empty or its URL fails origin validation — branch on that for a placeholder.
|
|
167
|
-
Full option surface:
|
|
168
|
-
|
|
169
|
-
**`getMediaProps(value, options)`** → `{ src, alt, width?, height? }`
|
|
170
|
-
|
|
171
|
-
| Option | Description |
|
|
172
|
-
|--------|-------------|
|
|
173
|
-
| `mediaBaseUrl` | CDN image origin; validation fails closed without it |
|
|
174
|
-
| `transform` | `ImageTransformParams` — `{ width?, height?, format? ("auto" \| "webp" \| "jpeg" \| "png" \| "gif" \| "avif"), quality? }`, merged onto the validated URL, preserving the editor's crop params |
|
|
175
|
-
|
|
176
|
-
Spread the result onto your own element or `next/image` (with `remotePatterns` set to the
|
|
177
|
-
CDN origin — `next/image` fetches server-side).
|
|
178
|
-
|
|
179
|
-
**`<MediaImage />`** props
|
|
180
|
-
|
|
181
|
-
| Prop | Description |
|
|
182
|
-
|------|-------------|
|
|
183
|
-
| `image` | The field value |
|
|
184
|
-
| `mediaBaseUrl` / `transform` | As above |
|
|
185
|
-
| `alt` | Overrides the value's alt text |
|
|
186
|
-
| …anything else | All other `<img>` attributes (`className`, `loading`, `sizes`, …) pass through; `width`/`height` come from the value's captured dimensions |
|
|
187
|
-
|
|
188
|
-
**`<MediaFigure />`** props
|
|
189
|
-
|
|
190
|
-
| Prop | Description |
|
|
191
|
-
|------|-------------|
|
|
192
|
-
| `image` | The field value |
|
|
193
|
-
| `mediaBaseUrl` / `transform` | As above |
|
|
194
|
-
| `schema` | `MetadataFieldDef[]` (e.g. the fetched `GET /media/schema`) — pins figcaption field order and labels. Without it, the value's own string metadata keys render in key order, which can shift across saves |
|
|
195
|
-
| `className` / `captionClassName` | Styling hooks for the `<figure>` and `<figcaption>` |
|
|
196
|
-
|
|
197
|
-
`alt` renders on the `<img>`; every other non-empty string metadata field renders as an
|
|
198
|
-
escaped `<span data-field="…">` inside the `<figcaption>`.
|
|
199
|
-
|
|
200
|
-
### Stored field value formats
|
|
201
|
-
|
|
202
|
-
Basic mode — a CDN URL (new keys are `{siteId}/assets/{assetId}/{versionId}-{filename}`; the editor's crop is `?fit=cover&gravity=auto` for smart crop, `?fit=scale-down` for fit-in; a rich value's `url` may instead carry `?trim.…` from the custom cropper):
|
|
203
|
-
|
|
204
|
-
```
|
|
205
|
-
https://media.p1.pantheon.io/image/{siteId}/assets/{assetId}/{versionId}-{filename}?fit=cover&gravity=auto
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
Rich mode — a `MediaValue` object:
|
|
209
|
-
|
|
210
|
-
```jsonc
|
|
211
|
-
{
|
|
212
|
-
"assetId": "…", "versionId": "…",
|
|
213
|
-
"url": "https://media.p1.pantheon.io/image/{siteId}/assets/{assetId}/{versionId}-{filename}",
|
|
214
|
-
"width": 1600, "height": 900,
|
|
215
|
-
"alt": "A red barn at sunset",
|
|
216
|
-
"metaSchemaVersion": 1
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
Legacy documents may still hold the old `{siteId}/{workstreamId}/media/{timestamp}-{filename}` URL form; the render helpers accept it (as a string) unchanged.
|
|
221
|
-
|
|
222
|
-
---
|
|
223
|
-
|
|
224
|
-
## For site developers enabling the media library
|
|
225
|
-
|
|
226
|
-
If you are building a P1-powered site and want editors to have a media library in the Puck sidebar, install this package and add the media plugin alongside the standard CCR editor setup. `siteId` and `getAuthToken` are read automatically from the ambient `@pantheon-systems/puck-css` context (`P1PuckProvider` / `P1AuthProvider`) when the plugin is rendered inside a standard CCR editor — no wiring needed for those two. `workerUrl` and `workstreamId` are both optional too (see the options table below).
|
|
227
|
-
|
|
228
|
-
### Install
|
|
229
|
-
|
|
230
|
-
```sh
|
|
231
|
-
pnpm add @pantheon-systems/p1-media
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### Integration with puck-css
|
|
235
|
-
|
|
236
|
-
Pass the media plugin via `additionalPlugins` in `useP1Editor`. The hook handles stable plugin merging internally — no manual override wiring needed.
|
|
237
|
-
|
|
238
|
-
```tsx
|
|
239
|
-
import { Puck } from "@puckeditor/core";
|
|
240
|
-
import { createMediaPlugin } from "@pantheon-systems/p1-media";
|
|
241
|
-
import { useP1Editor } from "@pantheon-systems/puck-css";
|
|
242
|
-
|
|
243
|
-
// siteId and getAuthToken are read from the ambient P1PuckProvider /
|
|
244
|
-
// P1AuthProvider automatically — no need to pass or memoize them yourself.
|
|
245
|
-
const mediaPlugin = createMediaPlugin({});
|
|
246
|
-
|
|
247
|
-
function Editor({ documentPath, config }) {
|
|
248
|
-
const { loading, error, puckKey, puckProps } = useP1Editor({
|
|
249
|
-
documentPath,
|
|
250
|
-
puckConfig: config,
|
|
251
|
-
additionalPlugins: [mediaPlugin],
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
if (loading) return null;
|
|
255
|
-
if (error) return <div>Error: {error.message}</div>;
|
|
256
|
-
return <Puck key={puckKey} {...puckProps} />;
|
|
257
|
-
}
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
Rendering outside a `P1PuckProvider`/`P1AuthProvider` (or overriding either value) still works — pass `siteId`/`getAuthToken` explicitly and they take precedence over the ambient context:
|
|
261
|
-
|
|
262
|
-
```tsx
|
|
263
|
-
import { useMemo } from "react";
|
|
264
|
-
import { createMediaPlugin } from "@pantheon-systems/p1-media";
|
|
265
|
-
import { useP1Auth } from "@pantheon-systems/puck-css";
|
|
266
|
-
|
|
267
|
-
function Editor({ siteId }) {
|
|
268
|
-
const { getToken } = useP1Auth();
|
|
269
|
-
const mediaPlugin = useMemo(
|
|
270
|
-
() => createMediaPlugin({ siteId, getAuthToken: getToken }),
|
|
271
|
-
[siteId, getToken],
|
|
272
|
-
);
|
|
273
|
-
// ...
|
|
274
|
-
}
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
### `createMediaPlugin` options
|
|
278
|
-
|
|
279
|
-
| Option | Type | Description |
|
|
280
|
-
|--------|------|-------------|
|
|
281
|
-
| `workerUrl` | `string` (optional) | Base URL of the deployed p1-media Worker. Defaults to the production host (`https://media.p1.pantheon.io`) — override for sandbox/staging/local dev |
|
|
282
|
-
| `siteId` | `string` (optional) | Site UUID. Defaults to the ambient puck-css `P1PuckProvider` context — pass explicitly only to override it, or when rendering outside that provider |
|
|
283
|
-
| `workstreamId` | `string` (optional) | Accepted for forward-compat; the site-scoped worker doesn't read it for any scoping decision today, so there's no need to pass it |
|
|
284
|
-
| `getAuthToken` | `() => Promise<string \| null> \| string \| null` (optional) | Returns the CCR auth bearer token. Defaults to the ambient puck-css `P1AuthProvider` context's `getToken` — pass explicitly only to override it, or when rendering outside that provider |
|
|
285
|
-
| `fieldNamePatterns` | `RegExp[]` | Override the default field name patterns |
|
|
286
|
-
| `metadataFields` | `MetadataFieldDef[]` | Fallback metadata schema for the `p1-media` field when `GET /media/schema` is unavailable (defaults to `[{ name: "alt", label: "Alt text", type: "string" }]`) |
|
|
287
|
-
|
|
288
|
-
`@pantheon-systems/puck-css` (`>=0.4.0`) is a required peer dependency as of this
|
|
289
|
-
version, matching `@pantheon-systems/p1-ai-chat`'s convention — every real
|
|
290
|
-
consumer of this plugin is already a P1/CCR site that depends on it.
|
|
291
|
-
|
|
292
|
-
---
|
|
293
|
-
|
|
294
|
-
## Exports
|
|
295
|
-
|
|
296
|
-
```ts
|
|
297
|
-
import {
|
|
298
|
-
createMediaPlugin, // Plugin factory (for CCR integration)
|
|
299
|
-
createMediaFigureBlock, // Ready-made Puck component: p1-media field + MediaFigure render
|
|
300
|
-
buildImageUrl, // Apply transform params to a CDN URL string (basic mode)
|
|
301
|
-
getMediaProps, // (value, { mediaBaseUrl, transform }) -> { src, alt, width?, height? }
|
|
302
|
-
MediaImage, // <img>-like component for a MediaValue
|
|
303
|
-
MediaFigure, // figure + schema-driven caption/credit/byline
|
|
304
|
-
makeMediaValue, // build a MediaValue (enforces the assetId+versionId invariant)
|
|
305
|
-
isMediaValue, // narrow string | MediaValue
|
|
306
|
-
DEFAULT_MEDIA_PATTERNS, // default field-name patterns
|
|
307
|
-
} from "@pantheon-systems/p1-media";
|
|
308
|
-
|
|
309
|
-
import type {
|
|
310
|
-
MediaPluginOptions, // createMediaPlugin config shape
|
|
311
|
-
MediaFigureBlockOptions, // createMediaFigureBlock config shape
|
|
312
|
-
MediaFigureBlockProps, // { photo: MediaFieldValue | null }
|
|
313
|
-
ImageTransformParams, // { width?, height?, format? (incl. "auto"), quality? }
|
|
314
|
-
MediaValue, // rich field value: { assetId, versionId, url, alt?, ... }
|
|
315
|
-
MediaFieldValue, // string | MediaValue
|
|
316
|
-
MediaProps, // getMediaProps return shape
|
|
317
|
-
MetadataFieldDef, // { name, label, type, required? }
|
|
318
|
-
GetMediaPropsOptions, // { mediaBaseUrl, transform? }
|
|
319
|
-
} from "@pantheon-systems/p1-media";
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
The server-safe subset (`buildImageUrl`, `getMediaProps`, `MediaImage`, `MediaFigure`, `createMediaFigureBlock`, `makeMediaValue`, `isMediaValue`, and the types) is also exported from the package's `react-server` entry for use in RSC. The interactive cropper (`react-image-crop`) is bundled into the client entry with its styles injected at runtime — consumers install nothing extra.
|