@pantheon-systems/p1-media 0.4.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.
@@ -0,0 +1,272 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactElement, ImgHTMLAttributes, ReactNode } from 'react';
3
+ import { ComponentConfig } from '@puckeditor/core';
4
+
5
+ /**
6
+ * A metadata field advertised by the Worker's `GET /media/schema` endpoint.
7
+ * The set is Pantheon-defined and global for v1; the plugin renders one input
8
+ * per entry and iterates these to render metadata generically (req. R14).
9
+ */
10
+ interface MetadataFieldDef {
11
+ name: string;
12
+ label: string;
13
+ type: "string";
14
+ required?: boolean;
15
+ }
16
+ /**
17
+ * The value written by the `p1-media` field (rich mode). Carries the pinned
18
+ * asset identity plus a snapshot of the metadata defaults copied at edit time.
19
+ * `metaSchemaVersion` records the schema that produced the snapshot (req. R12).
20
+ * `alt` and any schema-driven fields (byline, caption, …) live alongside as
21
+ * string metadata; `width`/`height` may be present as captured numeric
22
+ * dimensions (a CLS win when rendered).
23
+ */
24
+ interface MediaValue {
25
+ assetId: string;
26
+ versionId: string;
27
+ url: string;
28
+ metaSchemaVersion?: number;
29
+ alt?: string;
30
+ [meta: string]: string | number | undefined;
31
+ }
32
+ /**
33
+ * string = basic mode (a bare CDN URL, kept first-class forever); object =
34
+ * rich mode. Every render helper accepts both.
35
+ */
36
+ type MediaFieldValue = string | MediaValue;
37
+ /** Props produced by getMediaProps, spreadable onto `<img>` or next/image. */
38
+ interface MediaProps {
39
+ src: string;
40
+ alt: string;
41
+ width?: number;
42
+ height?: number;
43
+ }
44
+
45
+ interface MediaConfig {
46
+ workerUrl: string;
47
+ siteId: string;
48
+ workstreamId?: string;
49
+ getAuthToken: () => Promise<string | null> | string | null;
50
+ /**
51
+ * Fallback metadata field schema for the rich `p1-media` field, used when
52
+ * `GET /media/schema` is absent/unreachable (lets the plugin ship before the
53
+ * Worker upgrade). Defaults to `[{ name: "alt", label: "Alt text", type: "string" }]`.
54
+ */
55
+ metadataFields?: MetadataFieldDef[];
56
+ }
57
+
58
+ type GetAuthToken = () => Promise<string | null> | string | null;
59
+
60
+ interface MediaPluginOptions {
61
+ /** The base URL of the Cloudflare Worker media API. Defaults to the production host. */
62
+ workerUrl?: string;
63
+ /**
64
+ * Site identifier used to scope media to a specific site. Defaults to the
65
+ * ambient puck-css site context (`P1PuckProvider`) when omitted — pass this
66
+ * explicitly only when rendering outside that provider, or to override it.
67
+ */
68
+ siteId?: string;
69
+ /**
70
+ * Workstream (branch) identifier. Currently accepted for forward-compat but not
71
+ * read by the Worker for any scoping decision — omit unless a future release
72
+ * documents otherwise.
73
+ */
74
+ workstreamId?: string;
75
+ /**
76
+ * Function that returns the current auth token, or null if unauthenticated.
77
+ * May be async. Defaults to the ambient puck-css auth context's `getToken`
78
+ * (`P1AuthProvider`) when omitted — pass this explicitly only when rendering
79
+ * outside that provider, or to override it.
80
+ */
81
+ getAuthToken?: GetAuthToken;
82
+ /** Field name patterns that trigger the media picker (defaults to common image URL patterns) */
83
+ fieldNamePatterns?: RegExp[];
84
+ /**
85
+ * Fallback metadata field schema for the rich `p1-media` field, used when
86
+ * `GET /media/schema` is unavailable. Defaults to
87
+ * `[{ name: "alt", label: "Alt text", type: "string" }]`.
88
+ */
89
+ metadataFields?: MetadataFieldDef[];
90
+ }
91
+ /**
92
+ * Creates a Puck plugin that adds a media library backed by Cloudflare R2 + D1.
93
+ * It supports two field modes:
94
+ * - Basic: text fields matching image/media name patterns are replaced with the
95
+ * picker and store a clean CDN URL string. Render with `buildImageUrl()`.
96
+ * - Rich: a registered `p1-media` field type stores a MediaValue object (version
97
+ * URL + metadata such as alt). Render with `getMediaProps()` / `MediaImage` /
98
+ * `MediaFigure`. The editor's crop intent is carried as `?fit=…&gravity=…`.
99
+ *
100
+ * @example
101
+ * ```tsx
102
+ * // Inside a P1 site (rendered within P1PuckProvider + P1AuthProvider):
103
+ * // siteId and getAuthToken are read from context automatically.
104
+ * const mediaPlugin = createMediaPlugin({});
105
+ *
106
+ * // Outside a P1 site, or to override the ambient context, pass explicitly:
107
+ * const mediaPlugin = createMediaPlugin({
108
+ * siteId: "my-site",
109
+ * getAuthToken: () => localStorage.getItem("token"),
110
+ * });
111
+ *
112
+ * <Puck plugins={[mediaPlugin]} config={config} data={data} />
113
+ * ```
114
+ */
115
+ declare function createMediaPlugin(options: MediaPluginOptions): {
116
+ name: string;
117
+ overrides: {
118
+ fieldTypes: {
119
+ text: (props: any) => react_jsx_runtime.JSX.Element;
120
+ "p1-media": (props: any) => react_jsx_runtime.JSX.Element;
121
+ };
122
+ };
123
+ fieldTransforms: {
124
+ "p1-media": ({ value }: {
125
+ value: MediaFieldValue;
126
+ }) => MediaValue | {
127
+ url: string;
128
+ alt: string;
129
+ };
130
+ };
131
+ };
132
+
133
+ /**
134
+ * Default field name patterns that trigger the media library picker.
135
+ * Targets image/logo/icon source fields while excluding navigation URLs
136
+ * (buttonUrl, linkUrl, ctaUrl, etc.) and alt text fields.
137
+ */
138
+ declare const DEFAULT_MEDIA_PATTERNS: RegExp[];
139
+
140
+ interface ImageTransformParams {
141
+ width?: number;
142
+ height?: number;
143
+ /** `auto` negotiates avif → webp → jpeg from the request's Accept header. */
144
+ format?: "auto" | "webp" | "jpeg" | "png" | "gif" | "avif";
145
+ quality?: number;
146
+ }
147
+ /**
148
+ * Merges Cloudflare Images transform params onto a CDN image URL.
149
+ * Preserves any existing params in the URL — e.g. the editor's crop intent
150
+ * (`fit=cover&gravity=auto` for smart crop, `trim.*` for a manual crop).
151
+ *
152
+ * @example
153
+ * buildImageUrl(data.heroImage, { width: 1200, height: 630, format: "webp" })
154
+ */
155
+ declare function buildImageUrl(url: string, params: ImageTransformParams): string;
156
+
157
+ interface GetMediaPropsOptions {
158
+ /**
159
+ * The CDN image origin that serves media, e.g.
160
+ * "https://staging.media.p1.pantheon.io". This is the public image host —
161
+ * NOT the Worker API URL (`…workers.dev`), whose origin would reject every
162
+ * real image URL.
163
+ *
164
+ * Security (required): a value's `url` is untrusted document content — anyone
165
+ * who can edit a document, or call the CCR `/edits` API, controls it. Without
166
+ * an origin check a crafted `https://evil.example/beacon.png` turns every
167
+ * published render into a visitor-IP exfil beacon (and an SSRF under
168
+ * server-fetching `next/image`). getMediaProps therefore rejects any url that
169
+ * is not `https` on this exact origin. If `mediaBaseUrl` is omitted it
170
+ * **fails closed** (empty src) rather than degrading to an insecure pass-through.
171
+ *
172
+ * Local dev exception: when the configured base is itself `http` on a
173
+ * loopback host (`localhost`, `127.0.0.1`, `[::1]` — a local `wrangler dev`
174
+ * worker), same-origin `http` urls are allowed so rich values render locally.
175
+ */
176
+ mediaBaseUrl?: string;
177
+ /** Transform params merged onto the validated URL (width, height, format, quality). */
178
+ transform?: ImageTransformParams;
179
+ }
180
+ /**
181
+ * Normalizes a `string | MediaValue | null` into `{ src, alt, width?, height? }`.
182
+ * A string (basic mode) yields `alt: ""`. `src` is validated against the CDN
183
+ * origin (see GetMediaPropsOptions) and empty on rejection.
184
+ */
185
+ declare function getMediaProps(value: MediaFieldValue | null | undefined, options?: GetMediaPropsOptions): MediaProps;
186
+ /**
187
+ * Common-case `<img>` wrapper. Renders nothing when the src is rejected/empty
188
+ * (a broken foreign-origin image is never emitted). `alt` may be overridden;
189
+ * otherwise it comes from the value.
190
+ */
191
+ declare function MediaImage({ image, mediaBaseUrl, transform, alt, ...rest }: {
192
+ image: MediaFieldValue | null | undefined;
193
+ mediaBaseUrl?: string;
194
+ transform?: ImageTransformParams;
195
+ } & Omit<ImgHTMLAttributes<HTMLImageElement>, "src">): ReactElement | null;
196
+ /**
197
+ * `<figure>` that renders the image plus the schema-advertised text fields as
198
+ * ESCAPED text (React default escaping — never dangerouslySetInnerHTML, req.
199
+ * R6). Renders nothing when the src is rejected/empty.
200
+ */
201
+ declare function MediaFigure({ image, schema, mediaBaseUrl, transform, className, captionClassName, }: {
202
+ image: MediaFieldValue | null | undefined;
203
+ schema?: MetadataFieldDef[];
204
+ mediaBaseUrl?: string;
205
+ transform?: ImageTransformParams;
206
+ className?: string;
207
+ captionClassName?: string;
208
+ }): ReactElement | null;
209
+
210
+ interface MediaFigureBlockProps {
211
+ photo: MediaFieldValue | null;
212
+ }
213
+ interface MediaFigureBlockOptions {
214
+ /**
215
+ * The CDN image origin used to validate value URLs — NOT the Worker API
216
+ * URL. See GetMediaPropsOptions. Defaults to the production origin
217
+ * ("https://media.p1.pantheon.io"); override for sandbox/staging/local dev.
218
+ */
219
+ mediaBaseUrl?: string;
220
+ /**
221
+ * Render-time transform. Include BOTH width and height — the editor's crop
222
+ * intent only changes the output when there is a target aspect ratio.
223
+ * Defaults to `{ width: 1200, height: 630, format: "auto" }`.
224
+ */
225
+ transform?: ImageTransformParams;
226
+ /** Component label in the Puck sidebar. Defaults to "Media Figure". */
227
+ label?: string;
228
+ /** Label of the media field. Defaults to "Photo". */
229
+ fieldLabel?: string;
230
+ /** Metadata schema passed to MediaFigure — pins figcaption field order/labels. */
231
+ schema?: MetadataFieldDef[];
232
+ className?: string;
233
+ captionClassName?: string;
234
+ /** Rendered when no photo is chosen (or its URL fails origin validation). */
235
+ placeholder?: ReactNode;
236
+ }
237
+ /**
238
+ * Builds a registerable Puck component around the rich `p1-media` field and
239
+ * `MediaFigure`. Spares consumers the custom-field-type cast and the
240
+ * width+height transform pitfall:
241
+ *
242
+ * ```tsx
243
+ * const config = {
244
+ * components: {
245
+ * MediaFigureBlock: createMediaFigureBlock({ mediaBaseUrl: MEDIA_BASE }),
246
+ * },
247
+ * };
248
+ * ```
249
+ */
250
+ declare function createMediaFigureBlock(options: MediaFigureBlockOptions): ComponentConfig<MediaFigureBlockProps>;
251
+
252
+ /** Narrows a MediaFieldValue to the rich object form. */
253
+ declare function isMediaValue(value: MediaFieldValue | null | undefined): value is MediaValue;
254
+ /**
255
+ * Builds the value the `p1-media` field writes on asset selection.
256
+ *
257
+ * R10 invariant: never synthesize a MediaValue unless BOTH `assetId` and
258
+ * `versionId` are present — otherwise fall back to a bare URL string (basic
259
+ * mode). A value with undefined identity is unfindable by "update usages" and
260
+ * can render `src=undefined`, so this fallback is load-bearing, not cosmetic.
261
+ * It also means picking from a pre-upgrade Worker (bare-array `GET /media`, no
262
+ * assetId) yields a string, so the plugin degrades cleanly before the cutover.
263
+ */
264
+ declare function makeMediaValue(input: {
265
+ assetId?: string | null;
266
+ versionId?: string | null;
267
+ url: string;
268
+ metaSchemaVersion?: number;
269
+ metadata?: Record<string, string | number | undefined>;
270
+ }): MediaFieldValue;
271
+
272
+ export { DEFAULT_MEDIA_PATTERNS, type GetMediaPropsOptions, type ImageTransformParams, type MediaConfig, type MediaFieldValue, MediaFigure, type MediaFigureBlockOptions, type MediaFigureBlockProps, MediaImage, type MediaPluginOptions, type MediaProps, type MediaValue, type MetadataFieldDef, buildImageUrl, createMediaFigureBlock, createMediaPlugin, getMediaProps, isMediaValue, makeMediaValue };