@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.
- package/README.md +322 -0
- package/dist/index.d.mts +272 -0
- package/dist/index.d.ts +272 -0
- package/dist/index.js +2940 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2897 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server.d.mts +176 -0
- package/dist/server.d.ts +176 -0
- package/dist/server.js +246 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +213 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { ReactElement, ImgHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import { ComponentConfig } from '@puckeditor/core';
|
|
3
|
+
|
|
4
|
+
interface ImageTransformParams {
|
|
5
|
+
width?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
/** `auto` negotiates avif → webp → jpeg from the request's Accept header. */
|
|
8
|
+
format?: "auto" | "webp" | "jpeg" | "png" | "gif" | "avif";
|
|
9
|
+
quality?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Merges Cloudflare Images transform params onto a CDN image URL.
|
|
13
|
+
* Preserves any existing params in the URL — e.g. the editor's crop intent
|
|
14
|
+
* (`fit=cover&gravity=auto` for smart crop, `trim.*` for a manual crop).
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* buildImageUrl(data.heroImage, { width: 1200, height: 630, format: "webp" })
|
|
18
|
+
*/
|
|
19
|
+
declare function buildImageUrl(url: string, params: ImageTransformParams): string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A metadata field advertised by the Worker's `GET /media/schema` endpoint.
|
|
23
|
+
* The set is Pantheon-defined and global for v1; the plugin renders one input
|
|
24
|
+
* per entry and iterates these to render metadata generically (req. R14).
|
|
25
|
+
*/
|
|
26
|
+
interface MetadataFieldDef {
|
|
27
|
+
name: string;
|
|
28
|
+
label: string;
|
|
29
|
+
type: "string";
|
|
30
|
+
required?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The value written by the `p1-media` field (rich mode). Carries the pinned
|
|
34
|
+
* asset identity plus a snapshot of the metadata defaults copied at edit time.
|
|
35
|
+
* `metaSchemaVersion` records the schema that produced the snapshot (req. R12).
|
|
36
|
+
* `alt` and any schema-driven fields (byline, caption, …) live alongside as
|
|
37
|
+
* string metadata; `width`/`height` may be present as captured numeric
|
|
38
|
+
* dimensions (a CLS win when rendered).
|
|
39
|
+
*/
|
|
40
|
+
interface MediaValue {
|
|
41
|
+
assetId: string;
|
|
42
|
+
versionId: string;
|
|
43
|
+
url: string;
|
|
44
|
+
metaSchemaVersion?: number;
|
|
45
|
+
alt?: string;
|
|
46
|
+
[meta: string]: string | number | undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* string = basic mode (a bare CDN URL, kept first-class forever); object =
|
|
50
|
+
* rich mode. Every render helper accepts both.
|
|
51
|
+
*/
|
|
52
|
+
type MediaFieldValue = string | MediaValue;
|
|
53
|
+
/** Props produced by getMediaProps, spreadable onto `<img>` or next/image. */
|
|
54
|
+
interface MediaProps {
|
|
55
|
+
src: string;
|
|
56
|
+
alt: string;
|
|
57
|
+
width?: number;
|
|
58
|
+
height?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface GetMediaPropsOptions {
|
|
62
|
+
/**
|
|
63
|
+
* The CDN image origin that serves media, e.g.
|
|
64
|
+
* "https://staging.media.p1.pantheon.io". This is the public image host —
|
|
65
|
+
* NOT the Worker API URL (`…workers.dev`), whose origin would reject every
|
|
66
|
+
* real image URL.
|
|
67
|
+
*
|
|
68
|
+
* Security (required): a value's `url` is untrusted document content — anyone
|
|
69
|
+
* who can edit a document, or call the CCR `/edits` API, controls it. Without
|
|
70
|
+
* an origin check a crafted `https://evil.example/beacon.png` turns every
|
|
71
|
+
* published render into a visitor-IP exfil beacon (and an SSRF under
|
|
72
|
+
* server-fetching `next/image`). getMediaProps therefore rejects any url that
|
|
73
|
+
* is not `https` on this exact origin. If `mediaBaseUrl` is omitted it
|
|
74
|
+
* **fails closed** (empty src) rather than degrading to an insecure pass-through.
|
|
75
|
+
*
|
|
76
|
+
* Local dev exception: when the configured base is itself `http` on a
|
|
77
|
+
* loopback host (`localhost`, `127.0.0.1`, `[::1]` — a local `wrangler dev`
|
|
78
|
+
* worker), same-origin `http` urls are allowed so rich values render locally.
|
|
79
|
+
*/
|
|
80
|
+
mediaBaseUrl?: string;
|
|
81
|
+
/** Transform params merged onto the validated URL (width, height, format, quality). */
|
|
82
|
+
transform?: ImageTransformParams;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Normalizes a `string | MediaValue | null` into `{ src, alt, width?, height? }`.
|
|
86
|
+
* A string (basic mode) yields `alt: ""`. `src` is validated against the CDN
|
|
87
|
+
* origin (see GetMediaPropsOptions) and empty on rejection.
|
|
88
|
+
*/
|
|
89
|
+
declare function getMediaProps(value: MediaFieldValue | null | undefined, options?: GetMediaPropsOptions): MediaProps;
|
|
90
|
+
/**
|
|
91
|
+
* Common-case `<img>` wrapper. Renders nothing when the src is rejected/empty
|
|
92
|
+
* (a broken foreign-origin image is never emitted). `alt` may be overridden;
|
|
93
|
+
* otherwise it comes from the value.
|
|
94
|
+
*/
|
|
95
|
+
declare function MediaImage({ image, mediaBaseUrl, transform, alt, ...rest }: {
|
|
96
|
+
image: MediaFieldValue | null | undefined;
|
|
97
|
+
mediaBaseUrl?: string;
|
|
98
|
+
transform?: ImageTransformParams;
|
|
99
|
+
} & Omit<ImgHTMLAttributes<HTMLImageElement>, "src">): ReactElement | null;
|
|
100
|
+
/**
|
|
101
|
+
* `<figure>` that renders the image plus the schema-advertised text fields as
|
|
102
|
+
* ESCAPED text (React default escaping — never dangerouslySetInnerHTML, req.
|
|
103
|
+
* R6). Renders nothing when the src is rejected/empty.
|
|
104
|
+
*/
|
|
105
|
+
declare function MediaFigure({ image, schema, mediaBaseUrl, transform, className, captionClassName, }: {
|
|
106
|
+
image: MediaFieldValue | null | undefined;
|
|
107
|
+
schema?: MetadataFieldDef[];
|
|
108
|
+
mediaBaseUrl?: string;
|
|
109
|
+
transform?: ImageTransformParams;
|
|
110
|
+
className?: string;
|
|
111
|
+
captionClassName?: string;
|
|
112
|
+
}): ReactElement | null;
|
|
113
|
+
|
|
114
|
+
interface MediaFigureBlockProps {
|
|
115
|
+
photo: MediaFieldValue | null;
|
|
116
|
+
}
|
|
117
|
+
interface MediaFigureBlockOptions {
|
|
118
|
+
/**
|
|
119
|
+
* The CDN image origin used to validate value URLs — NOT the Worker API
|
|
120
|
+
* URL. See GetMediaPropsOptions. Defaults to the production origin
|
|
121
|
+
* ("https://media.p1.pantheon.io"); override for sandbox/staging/local dev.
|
|
122
|
+
*/
|
|
123
|
+
mediaBaseUrl?: string;
|
|
124
|
+
/**
|
|
125
|
+
* Render-time transform. Include BOTH width and height — the editor's crop
|
|
126
|
+
* intent only changes the output when there is a target aspect ratio.
|
|
127
|
+
* Defaults to `{ width: 1200, height: 630, format: "auto" }`.
|
|
128
|
+
*/
|
|
129
|
+
transform?: ImageTransformParams;
|
|
130
|
+
/** Component label in the Puck sidebar. Defaults to "Media Figure". */
|
|
131
|
+
label?: string;
|
|
132
|
+
/** Label of the media field. Defaults to "Photo". */
|
|
133
|
+
fieldLabel?: string;
|
|
134
|
+
/** Metadata schema passed to MediaFigure — pins figcaption field order/labels. */
|
|
135
|
+
schema?: MetadataFieldDef[];
|
|
136
|
+
className?: string;
|
|
137
|
+
captionClassName?: string;
|
|
138
|
+
/** Rendered when no photo is chosen (or its URL fails origin validation). */
|
|
139
|
+
placeholder?: ReactNode;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Builds a registerable Puck component around the rich `p1-media` field and
|
|
143
|
+
* `MediaFigure`. Spares consumers the custom-field-type cast and the
|
|
144
|
+
* width+height transform pitfall:
|
|
145
|
+
*
|
|
146
|
+
* ```tsx
|
|
147
|
+
* const config = {
|
|
148
|
+
* components: {
|
|
149
|
+
* MediaFigureBlock: createMediaFigureBlock({ mediaBaseUrl: MEDIA_BASE }),
|
|
150
|
+
* },
|
|
151
|
+
* };
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
declare function createMediaFigureBlock(options: MediaFigureBlockOptions): ComponentConfig<MediaFigureBlockProps>;
|
|
155
|
+
|
|
156
|
+
/** Narrows a MediaFieldValue to the rich object form. */
|
|
157
|
+
declare function isMediaValue(value: MediaFieldValue | null | undefined): value is MediaValue;
|
|
158
|
+
/**
|
|
159
|
+
* Builds the value the `p1-media` field writes on asset selection.
|
|
160
|
+
*
|
|
161
|
+
* R10 invariant: never synthesize a MediaValue unless BOTH `assetId` and
|
|
162
|
+
* `versionId` are present — otherwise fall back to a bare URL string (basic
|
|
163
|
+
* mode). A value with undefined identity is unfindable by "update usages" and
|
|
164
|
+
* can render `src=undefined`, so this fallback is load-bearing, not cosmetic.
|
|
165
|
+
* It also means picking from a pre-upgrade Worker (bare-array `GET /media`, no
|
|
166
|
+
* assetId) yields a string, so the plugin degrades cleanly before the cutover.
|
|
167
|
+
*/
|
|
168
|
+
declare function makeMediaValue(input: {
|
|
169
|
+
assetId?: string | null;
|
|
170
|
+
versionId?: string | null;
|
|
171
|
+
url: string;
|
|
172
|
+
metaSchemaVersion?: number;
|
|
173
|
+
metadata?: Record<string, string | number | undefined>;
|
|
174
|
+
}): MediaFieldValue;
|
|
175
|
+
|
|
176
|
+
export { type GetMediaPropsOptions, type ImageTransformParams, type MediaFieldValue, MediaFigure, type MediaFigureBlockOptions, type MediaFigureBlockProps, MediaImage, type MediaProps, type MediaValue, type MetadataFieldDef, buildImageUrl, createMediaFigureBlock, getMediaProps, isMediaValue, makeMediaValue };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { ReactElement, ImgHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import { ComponentConfig } from '@puckeditor/core';
|
|
3
|
+
|
|
4
|
+
interface ImageTransformParams {
|
|
5
|
+
width?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
/** `auto` negotiates avif → webp → jpeg from the request's Accept header. */
|
|
8
|
+
format?: "auto" | "webp" | "jpeg" | "png" | "gif" | "avif";
|
|
9
|
+
quality?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Merges Cloudflare Images transform params onto a CDN image URL.
|
|
13
|
+
* Preserves any existing params in the URL — e.g. the editor's crop intent
|
|
14
|
+
* (`fit=cover&gravity=auto` for smart crop, `trim.*` for a manual crop).
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* buildImageUrl(data.heroImage, { width: 1200, height: 630, format: "webp" })
|
|
18
|
+
*/
|
|
19
|
+
declare function buildImageUrl(url: string, params: ImageTransformParams): string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A metadata field advertised by the Worker's `GET /media/schema` endpoint.
|
|
23
|
+
* The set is Pantheon-defined and global for v1; the plugin renders one input
|
|
24
|
+
* per entry and iterates these to render metadata generically (req. R14).
|
|
25
|
+
*/
|
|
26
|
+
interface MetadataFieldDef {
|
|
27
|
+
name: string;
|
|
28
|
+
label: string;
|
|
29
|
+
type: "string";
|
|
30
|
+
required?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The value written by the `p1-media` field (rich mode). Carries the pinned
|
|
34
|
+
* asset identity plus a snapshot of the metadata defaults copied at edit time.
|
|
35
|
+
* `metaSchemaVersion` records the schema that produced the snapshot (req. R12).
|
|
36
|
+
* `alt` and any schema-driven fields (byline, caption, …) live alongside as
|
|
37
|
+
* string metadata; `width`/`height` may be present as captured numeric
|
|
38
|
+
* dimensions (a CLS win when rendered).
|
|
39
|
+
*/
|
|
40
|
+
interface MediaValue {
|
|
41
|
+
assetId: string;
|
|
42
|
+
versionId: string;
|
|
43
|
+
url: string;
|
|
44
|
+
metaSchemaVersion?: number;
|
|
45
|
+
alt?: string;
|
|
46
|
+
[meta: string]: string | number | undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* string = basic mode (a bare CDN URL, kept first-class forever); object =
|
|
50
|
+
* rich mode. Every render helper accepts both.
|
|
51
|
+
*/
|
|
52
|
+
type MediaFieldValue = string | MediaValue;
|
|
53
|
+
/** Props produced by getMediaProps, spreadable onto `<img>` or next/image. */
|
|
54
|
+
interface MediaProps {
|
|
55
|
+
src: string;
|
|
56
|
+
alt: string;
|
|
57
|
+
width?: number;
|
|
58
|
+
height?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface GetMediaPropsOptions {
|
|
62
|
+
/**
|
|
63
|
+
* The CDN image origin that serves media, e.g.
|
|
64
|
+
* "https://staging.media.p1.pantheon.io". This is the public image host —
|
|
65
|
+
* NOT the Worker API URL (`…workers.dev`), whose origin would reject every
|
|
66
|
+
* real image URL.
|
|
67
|
+
*
|
|
68
|
+
* Security (required): a value's `url` is untrusted document content — anyone
|
|
69
|
+
* who can edit a document, or call the CCR `/edits` API, controls it. Without
|
|
70
|
+
* an origin check a crafted `https://evil.example/beacon.png` turns every
|
|
71
|
+
* published render into a visitor-IP exfil beacon (and an SSRF under
|
|
72
|
+
* server-fetching `next/image`). getMediaProps therefore rejects any url that
|
|
73
|
+
* is not `https` on this exact origin. If `mediaBaseUrl` is omitted it
|
|
74
|
+
* **fails closed** (empty src) rather than degrading to an insecure pass-through.
|
|
75
|
+
*
|
|
76
|
+
* Local dev exception: when the configured base is itself `http` on a
|
|
77
|
+
* loopback host (`localhost`, `127.0.0.1`, `[::1]` — a local `wrangler dev`
|
|
78
|
+
* worker), same-origin `http` urls are allowed so rich values render locally.
|
|
79
|
+
*/
|
|
80
|
+
mediaBaseUrl?: string;
|
|
81
|
+
/** Transform params merged onto the validated URL (width, height, format, quality). */
|
|
82
|
+
transform?: ImageTransformParams;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Normalizes a `string | MediaValue | null` into `{ src, alt, width?, height? }`.
|
|
86
|
+
* A string (basic mode) yields `alt: ""`. `src` is validated against the CDN
|
|
87
|
+
* origin (see GetMediaPropsOptions) and empty on rejection.
|
|
88
|
+
*/
|
|
89
|
+
declare function getMediaProps(value: MediaFieldValue | null | undefined, options?: GetMediaPropsOptions): MediaProps;
|
|
90
|
+
/**
|
|
91
|
+
* Common-case `<img>` wrapper. Renders nothing when the src is rejected/empty
|
|
92
|
+
* (a broken foreign-origin image is never emitted). `alt` may be overridden;
|
|
93
|
+
* otherwise it comes from the value.
|
|
94
|
+
*/
|
|
95
|
+
declare function MediaImage({ image, mediaBaseUrl, transform, alt, ...rest }: {
|
|
96
|
+
image: MediaFieldValue | null | undefined;
|
|
97
|
+
mediaBaseUrl?: string;
|
|
98
|
+
transform?: ImageTransformParams;
|
|
99
|
+
} & Omit<ImgHTMLAttributes<HTMLImageElement>, "src">): ReactElement | null;
|
|
100
|
+
/**
|
|
101
|
+
* `<figure>` that renders the image plus the schema-advertised text fields as
|
|
102
|
+
* ESCAPED text (React default escaping — never dangerouslySetInnerHTML, req.
|
|
103
|
+
* R6). Renders nothing when the src is rejected/empty.
|
|
104
|
+
*/
|
|
105
|
+
declare function MediaFigure({ image, schema, mediaBaseUrl, transform, className, captionClassName, }: {
|
|
106
|
+
image: MediaFieldValue | null | undefined;
|
|
107
|
+
schema?: MetadataFieldDef[];
|
|
108
|
+
mediaBaseUrl?: string;
|
|
109
|
+
transform?: ImageTransformParams;
|
|
110
|
+
className?: string;
|
|
111
|
+
captionClassName?: string;
|
|
112
|
+
}): ReactElement | null;
|
|
113
|
+
|
|
114
|
+
interface MediaFigureBlockProps {
|
|
115
|
+
photo: MediaFieldValue | null;
|
|
116
|
+
}
|
|
117
|
+
interface MediaFigureBlockOptions {
|
|
118
|
+
/**
|
|
119
|
+
* The CDN image origin used to validate value URLs — NOT the Worker API
|
|
120
|
+
* URL. See GetMediaPropsOptions. Defaults to the production origin
|
|
121
|
+
* ("https://media.p1.pantheon.io"); override for sandbox/staging/local dev.
|
|
122
|
+
*/
|
|
123
|
+
mediaBaseUrl?: string;
|
|
124
|
+
/**
|
|
125
|
+
* Render-time transform. Include BOTH width and height — the editor's crop
|
|
126
|
+
* intent only changes the output when there is a target aspect ratio.
|
|
127
|
+
* Defaults to `{ width: 1200, height: 630, format: "auto" }`.
|
|
128
|
+
*/
|
|
129
|
+
transform?: ImageTransformParams;
|
|
130
|
+
/** Component label in the Puck sidebar. Defaults to "Media Figure". */
|
|
131
|
+
label?: string;
|
|
132
|
+
/** Label of the media field. Defaults to "Photo". */
|
|
133
|
+
fieldLabel?: string;
|
|
134
|
+
/** Metadata schema passed to MediaFigure — pins figcaption field order/labels. */
|
|
135
|
+
schema?: MetadataFieldDef[];
|
|
136
|
+
className?: string;
|
|
137
|
+
captionClassName?: string;
|
|
138
|
+
/** Rendered when no photo is chosen (or its URL fails origin validation). */
|
|
139
|
+
placeholder?: ReactNode;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Builds a registerable Puck component around the rich `p1-media` field and
|
|
143
|
+
* `MediaFigure`. Spares consumers the custom-field-type cast and the
|
|
144
|
+
* width+height transform pitfall:
|
|
145
|
+
*
|
|
146
|
+
* ```tsx
|
|
147
|
+
* const config = {
|
|
148
|
+
* components: {
|
|
149
|
+
* MediaFigureBlock: createMediaFigureBlock({ mediaBaseUrl: MEDIA_BASE }),
|
|
150
|
+
* },
|
|
151
|
+
* };
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
declare function createMediaFigureBlock(options: MediaFigureBlockOptions): ComponentConfig<MediaFigureBlockProps>;
|
|
155
|
+
|
|
156
|
+
/** Narrows a MediaFieldValue to the rich object form. */
|
|
157
|
+
declare function isMediaValue(value: MediaFieldValue | null | undefined): value is MediaValue;
|
|
158
|
+
/**
|
|
159
|
+
* Builds the value the `p1-media` field writes on asset selection.
|
|
160
|
+
*
|
|
161
|
+
* R10 invariant: never synthesize a MediaValue unless BOTH `assetId` and
|
|
162
|
+
* `versionId` are present — otherwise fall back to a bare URL string (basic
|
|
163
|
+
* mode). A value with undefined identity is unfindable by "update usages" and
|
|
164
|
+
* can render `src=undefined`, so this fallback is load-bearing, not cosmetic.
|
|
165
|
+
* It also means picking from a pre-upgrade Worker (bare-array `GET /media`, no
|
|
166
|
+
* assetId) yields a string, so the plugin degrades cleanly before the cutover.
|
|
167
|
+
*/
|
|
168
|
+
declare function makeMediaValue(input: {
|
|
169
|
+
assetId?: string | null;
|
|
170
|
+
versionId?: string | null;
|
|
171
|
+
url: string;
|
|
172
|
+
metaSchemaVersion?: number;
|
|
173
|
+
metadata?: Record<string, string | number | undefined>;
|
|
174
|
+
}): MediaFieldValue;
|
|
175
|
+
|
|
176
|
+
export { type GetMediaPropsOptions, type ImageTransformParams, type MediaFieldValue, MediaFigure, type MediaFigureBlockOptions, type MediaFigureBlockProps, MediaImage, type MediaProps, type MediaValue, type MetadataFieldDef, buildImageUrl, createMediaFigureBlock, getMediaProps, isMediaValue, makeMediaValue };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server.ts
|
|
21
|
+
var server_exports = {};
|
|
22
|
+
__export(server_exports, {
|
|
23
|
+
MediaFigure: () => MediaFigure,
|
|
24
|
+
MediaImage: () => MediaImage,
|
|
25
|
+
buildImageUrl: () => buildImageUrl,
|
|
26
|
+
createMediaFigureBlock: () => createMediaFigureBlock,
|
|
27
|
+
getMediaProps: () => getMediaProps,
|
|
28
|
+
isMediaValue: () => isMediaValue,
|
|
29
|
+
makeMediaValue: () => makeMediaValue
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(server_exports);
|
|
32
|
+
|
|
33
|
+
// src/utils.ts
|
|
34
|
+
function buildImageUrl(url, params) {
|
|
35
|
+
if (!url) return url;
|
|
36
|
+
const parsed = new URL(url);
|
|
37
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return url;
|
|
38
|
+
for (const [k, v] of Object.entries(params)) {
|
|
39
|
+
if (v !== void 0) {
|
|
40
|
+
parsed.searchParams.set(k, String(v));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return parsed.toString();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/media-value.ts
|
|
47
|
+
var STRUCTURAL_MEDIA_KEYS = /* @__PURE__ */ new Set([
|
|
48
|
+
"assetId",
|
|
49
|
+
"versionId",
|
|
50
|
+
"url",
|
|
51
|
+
"metaSchemaVersion"
|
|
52
|
+
]);
|
|
53
|
+
function isMediaValue(value) {
|
|
54
|
+
return typeof value === "object" && value !== null;
|
|
55
|
+
}
|
|
56
|
+
function makeMediaValue(input) {
|
|
57
|
+
if (!input.assetId || !input.versionId) {
|
|
58
|
+
return input.url;
|
|
59
|
+
}
|
|
60
|
+
const value = {
|
|
61
|
+
assetId: input.assetId,
|
|
62
|
+
versionId: input.versionId,
|
|
63
|
+
url: input.url
|
|
64
|
+
};
|
|
65
|
+
if (typeof input.metaSchemaVersion === "number") {
|
|
66
|
+
value.metaSchemaVersion = input.metaSchemaVersion;
|
|
67
|
+
}
|
|
68
|
+
if (input.metadata) {
|
|
69
|
+
for (const [k, v] of Object.entries(input.metadata)) {
|
|
70
|
+
if (v === void 0 || v === "") continue;
|
|
71
|
+
if (STRUCTURAL_MEDIA_KEYS.has(k)) continue;
|
|
72
|
+
value[k] = v;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/render.tsx
|
|
79
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
80
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
81
|
+
function validateSrc(url, mediaBaseUrl) {
|
|
82
|
+
if (!url || !mediaBaseUrl) return "";
|
|
83
|
+
let parsed;
|
|
84
|
+
let base;
|
|
85
|
+
try {
|
|
86
|
+
parsed = new URL(url);
|
|
87
|
+
base = new URL(mediaBaseUrl);
|
|
88
|
+
} catch {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
if (parsed.origin !== base.origin) return "";
|
|
92
|
+
if (parsed.protocol !== "https:") {
|
|
93
|
+
const isLocalDevBase = base.protocol === "http:" && LOOPBACK_HOSTS.has(base.hostname);
|
|
94
|
+
if (!isLocalDevBase || parsed.protocol !== "http:") return "";
|
|
95
|
+
}
|
|
96
|
+
return url;
|
|
97
|
+
}
|
|
98
|
+
function getMediaProps(value, options) {
|
|
99
|
+
const mediaBaseUrl = options?.mediaBaseUrl;
|
|
100
|
+
const transform = options?.transform;
|
|
101
|
+
const finalize = (rawUrl) => {
|
|
102
|
+
const validated = validateSrc(rawUrl, mediaBaseUrl);
|
|
103
|
+
return validated && transform ? buildImageUrl(validated, transform) : validated;
|
|
104
|
+
};
|
|
105
|
+
if (value == null) return { src: "", alt: "" };
|
|
106
|
+
if (typeof value === "string") {
|
|
107
|
+
return { src: finalize(value), alt: "" };
|
|
108
|
+
}
|
|
109
|
+
const props = {
|
|
110
|
+
src: finalize(typeof value.url === "string" ? value.url : ""),
|
|
111
|
+
alt: typeof value.alt === "string" ? value.alt : ""
|
|
112
|
+
};
|
|
113
|
+
if (typeof value.width === "number") props.width = value.width;
|
|
114
|
+
if (typeof value.height === "number") props.height = value.height;
|
|
115
|
+
return props;
|
|
116
|
+
}
|
|
117
|
+
function MediaImage({
|
|
118
|
+
image,
|
|
119
|
+
mediaBaseUrl,
|
|
120
|
+
transform,
|
|
121
|
+
alt,
|
|
122
|
+
...rest
|
|
123
|
+
}) {
|
|
124
|
+
const props = getMediaProps(image, { mediaBaseUrl, transform });
|
|
125
|
+
if (!props.src) return null;
|
|
126
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
127
|
+
"img",
|
|
128
|
+
{
|
|
129
|
+
src: props.src,
|
|
130
|
+
alt: alt ?? props.alt,
|
|
131
|
+
...props.width !== void 0 ? { width: props.width } : {},
|
|
132
|
+
...props.height !== void 0 ? { height: props.height } : {},
|
|
133
|
+
...rest
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
function collectFigureFields(image, schema) {
|
|
138
|
+
if (!isMediaValue(image)) return [];
|
|
139
|
+
const out = [];
|
|
140
|
+
if (schema && schema.length > 0) {
|
|
141
|
+
for (const entry of schema) {
|
|
142
|
+
if (entry.name === "alt") continue;
|
|
143
|
+
const raw = image[entry.name];
|
|
144
|
+
if (raw === void 0 || raw === "") continue;
|
|
145
|
+
out.push({ name: entry.name, label: entry.label, value: String(raw) });
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
for (const [k, v] of Object.entries(image)) {
|
|
149
|
+
if (k === "alt") continue;
|
|
150
|
+
if (STRUCTURAL_MEDIA_KEYS.has(k)) continue;
|
|
151
|
+
if (typeof v !== "string" || v === "") continue;
|
|
152
|
+
out.push({ name: k, value: v });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
function MediaFigure({
|
|
158
|
+
image,
|
|
159
|
+
schema,
|
|
160
|
+
mediaBaseUrl,
|
|
161
|
+
transform,
|
|
162
|
+
className,
|
|
163
|
+
captionClassName
|
|
164
|
+
}) {
|
|
165
|
+
const props = getMediaProps(image, { mediaBaseUrl, transform });
|
|
166
|
+
if (!props.src) return null;
|
|
167
|
+
const fields = collectFigureFields(image, schema);
|
|
168
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("figure", { className, children: [
|
|
169
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
170
|
+
"img",
|
|
171
|
+
{
|
|
172
|
+
src: props.src,
|
|
173
|
+
alt: props.alt,
|
|
174
|
+
...props.width !== void 0 ? { width: props.width } : {},
|
|
175
|
+
...props.height !== void 0 ? { height: props.height } : {}
|
|
176
|
+
}
|
|
177
|
+
),
|
|
178
|
+
fields.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("figcaption", { className: captionClassName, children: fields.map((f) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-field": f.name, children: f.value }, f.name)) })
|
|
179
|
+
] });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/media-figure-block.tsx
|
|
183
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
184
|
+
var DEFAULT_TRANSFORM = { width: 1200, height: 630, format: "auto" };
|
|
185
|
+
var DEFAULT_MEDIA_BASE_URL = "https://media.p1.pantheon.io";
|
|
186
|
+
var placeholderStyle = {
|
|
187
|
+
display: "flex",
|
|
188
|
+
alignItems: "center",
|
|
189
|
+
justifyContent: "center",
|
|
190
|
+
minHeight: "200px",
|
|
191
|
+
borderRadius: "8px",
|
|
192
|
+
backgroundColor: "#e5e5e5",
|
|
193
|
+
color: "#525252",
|
|
194
|
+
fontSize: "14px"
|
|
195
|
+
};
|
|
196
|
+
function createMediaFigureBlock(options) {
|
|
197
|
+
const {
|
|
198
|
+
mediaBaseUrl = DEFAULT_MEDIA_BASE_URL,
|
|
199
|
+
transform = DEFAULT_TRANSFORM,
|
|
200
|
+
label = "Media Figure",
|
|
201
|
+
fieldLabel = "Photo",
|
|
202
|
+
schema,
|
|
203
|
+
className,
|
|
204
|
+
captionClassName,
|
|
205
|
+
placeholder = "Choose a photo from the media library"
|
|
206
|
+
} = options;
|
|
207
|
+
return {
|
|
208
|
+
label,
|
|
209
|
+
fields: {
|
|
210
|
+
// `p1-media` is registered by createMediaPlugin via overrides.fieldTypes,
|
|
211
|
+
// so it is not part of Puck's built-in Field union.
|
|
212
|
+
photo: { type: "p1-media", label: fieldLabel }
|
|
213
|
+
},
|
|
214
|
+
defaultProps: {
|
|
215
|
+
photo: null
|
|
216
|
+
},
|
|
217
|
+
render: ({ photo }) => {
|
|
218
|
+
const { src } = getMediaProps(photo ?? null, { mediaBaseUrl });
|
|
219
|
+
if (!src) {
|
|
220
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: placeholderStyle, children: placeholder });
|
|
221
|
+
}
|
|
222
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
223
|
+
MediaFigure,
|
|
224
|
+
{
|
|
225
|
+
image: photo ?? null,
|
|
226
|
+
mediaBaseUrl,
|
|
227
|
+
transform,
|
|
228
|
+
schema,
|
|
229
|
+
className,
|
|
230
|
+
captionClassName
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
237
|
+
0 && (module.exports = {
|
|
238
|
+
MediaFigure,
|
|
239
|
+
MediaImage,
|
|
240
|
+
buildImageUrl,
|
|
241
|
+
createMediaFigureBlock,
|
|
242
|
+
getMediaProps,
|
|
243
|
+
isMediaValue,
|
|
244
|
+
makeMediaValue
|
|
245
|
+
});
|
|
246
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/utils.ts","../src/media-value.ts","../src/render.tsx","../src/media-figure-block.tsx"],"sourcesContent":["// Server-safe entry point — no React context, safe for RSC.\n// Next.js resolves this via the \"react-server\" exports condition.\nexport { buildImageUrl } from \"./utils\";\nexport type { ImageTransformParams } from \"./utils\";\nexport { getMediaProps, MediaImage, MediaFigure } from \"./render\";\nexport type { GetMediaPropsOptions } from \"./render\";\nexport { createMediaFigureBlock } from \"./media-figure-block\";\nexport type { MediaFigureBlockOptions, MediaFigureBlockProps } from \"./media-figure-block\";\nexport { makeMediaValue, isMediaValue } from \"./media-value\";\nexport type {\n MediaValue,\n MediaFieldValue,\n MediaProps,\n MetadataFieldDef,\n} from \"./types\";\n","export interface ImageTransformParams {\n width?: number;\n height?: number;\n /** `auto` negotiates avif → webp → jpeg from the request's Accept header. */\n format?: \"auto\" | \"webp\" | \"jpeg\" | \"png\" | \"gif\" | \"avif\";\n quality?: number;\n}\n\n/**\n * Merges Cloudflare Images transform params onto a CDN image URL.\n * Preserves any existing params in the URL — e.g. the editor's crop intent\n * (`fit=cover&gravity=auto` for smart crop, `trim.*` for a manual crop).\n *\n * @example\n * buildImageUrl(data.heroImage, { width: 1200, height: 630, format: \"webp\" })\n */\nexport function buildImageUrl(url: string, params: ImageTransformParams): string {\n if (!url) return url;\n const parsed = new URL(url);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return url;\n for (const [k, v] of Object.entries(params)) {\n if (v !== undefined) {\n parsed.searchParams.set(k, String(v));\n }\n }\n return parsed.toString();\n}\n","import type { MediaFieldValue, MediaValue } from \"./types\";\nimport {\n buildValueWithCrop,\n buildValueWithTrim,\n getBaseUrl,\n type CropMode,\n type TrimRect,\n} from \"./crop\";\n\n/**\n * Structural keys on a MediaValue that are not schema-driven metadata. Used to\n * avoid clobbering identity fields when copying metadata defaults into a value.\n */\nexport const STRUCTURAL_MEDIA_KEYS = new Set([\n \"assetId\",\n \"versionId\",\n \"url\",\n \"metaSchemaVersion\",\n]);\n\n/** Narrows a MediaFieldValue to the rich object form. */\nexport function isMediaValue(\n value: MediaFieldValue | null | undefined,\n): value is MediaValue {\n return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Builds the value the `p1-media` field writes on asset selection.\n *\n * R10 invariant: never synthesize a MediaValue unless BOTH `assetId` and\n * `versionId` are present — otherwise fall back to a bare URL string (basic\n * mode). A value with undefined identity is unfindable by \"update usages\" and\n * can render `src=undefined`, so this fallback is load-bearing, not cosmetic.\n * It also means picking from a pre-upgrade Worker (bare-array `GET /media`, no\n * assetId) yields a string, so the plugin degrades cleanly before the cutover.\n */\nexport function makeMediaValue(input: {\n assetId?: string | null;\n versionId?: string | null;\n url: string;\n metaSchemaVersion?: number;\n metadata?: Record<string, string | number | undefined>;\n}): MediaFieldValue {\n if (!input.assetId || !input.versionId) {\n return input.url; // R10 fallback: string value\n }\n const value: MediaValue = {\n assetId: input.assetId,\n versionId: input.versionId,\n url: input.url,\n };\n if (typeof input.metaSchemaVersion === \"number\") {\n value.metaSchemaVersion = input.metaSchemaVersion;\n }\n if (input.metadata) {\n for (const [k, v] of Object.entries(input.metadata)) {\n if (v === undefined || v === \"\") continue;\n if (STRUCTURAL_MEDIA_KEYS.has(k)) continue; // never let metadata clobber identity\n value[k] = v;\n }\n }\n return value;\n}\n\n/** The asset fields the field editor needs to build a value on selection. */\nexport interface SelectableAsset {\n assetId?: string | null;\n versionId?: string | null;\n url: string;\n width?: number;\n height?: number;\n metadata?: Record<string, string>;\n metaSchemaVersion?: number;\n}\n\n/**\n * Builds the value written when an asset is picked from the library, applying\n * the current crop and copying metadata (flat, incl. alt) plus captured\n * dimensions. Delegates to makeMediaValue, so R10 still holds (a pre-cutover\n * asset with no identity yields a string).\n */\nexport function buildValueFromAsset(\n asset: SelectableAsset,\n crop: CropMode,\n): MediaFieldValue {\n const metadata: Record<string, string | number | undefined> = { ...(asset.metadata ?? {}) };\n if (typeof asset.width === \"number\") metadata.width = asset.width;\n if (typeof asset.height === \"number\") metadata.height = asset.height;\n return makeMediaValue({\n assetId: asset.assetId,\n versionId: asset.versionId,\n url: buildValueWithCrop(getBaseUrl(asset.url), crop),\n metaSchemaVersion: asset.metaSchemaVersion,\n metadata,\n });\n}\n\n/**\n * Applies a crop mode to a field value. R10: a string stays a string (basic\n * mode) — crop never promotes a value to a partial-identity object.\n */\nexport function applyCropToValue(\n value: MediaFieldValue,\n crop: CropMode,\n): MediaFieldValue {\n const currentUrl = isMediaValue(value) ? value.url : value;\n const baseUrl = getBaseUrl(typeof currentUrl === \"string\" ? currentUrl : \"\");\n if (!baseUrl) return value;\n const url = buildValueWithCrop(baseUrl, crop);\n return isMediaValue(value) ? { ...value, url } : url;\n}\n\n/**\n * Applies a manual crop region (the interactive cropper's result) to a field\n * value. Same R10 shape as applyCropToValue: a string stays a string, an\n * object keeps its identity and only swaps `url`.\n */\nexport function applyTrimToValue(\n value: MediaFieldValue,\n rect: TrimRect,\n): MediaFieldValue {\n const currentUrl = isMediaValue(value) ? value.url : value;\n const baseUrl = getBaseUrl(typeof currentUrl === \"string\" ? currentUrl : \"\");\n if (!baseUrl) return value;\n const url = buildValueWithTrim(baseUrl, rect);\n return isMediaValue(value) ? { ...value, url } : url;\n}\n\n/**\n * Sets one metadata field on a value. R10: refuses to operate on a string\n * (returns it unchanged — metadata can't be attached until an asset is picked)\n * and never lets a structural key clobber the pinned identity.\n */\nexport function setMetaOnValue(\n value: MediaFieldValue,\n fieldName: string,\n fieldValue: string,\n): MediaFieldValue {\n if (!isMediaValue(value)) return value;\n if (STRUCTURAL_MEDIA_KEYS.has(fieldName)) return value;\n return { ...value, [fieldName]: fieldValue };\n}\n","// Render-side helpers. Server-safe (no React context, no client-only hooks) so\n// they can be imported from RSC via the package's \"react-server\" export.\nimport type { ImgHTMLAttributes, ReactElement } from \"react\";\nimport type {\n MediaFieldValue,\n MediaProps,\n MetadataFieldDef,\n} from \"./types\";\nimport { isMediaValue, STRUCTURAL_MEDIA_KEYS } from \"./media-value\";\nimport { buildImageUrl, type ImageTransformParams } from \"./utils\";\n\nexport interface GetMediaPropsOptions {\n /**\n * The CDN image origin that serves media, e.g.\n * \"https://staging.media.p1.pantheon.io\". This is the public image host —\n * NOT the Worker API URL (`…workers.dev`), whose origin would reject every\n * real image URL.\n *\n * Security (required): a value's `url` is untrusted document content — anyone\n * who can edit a document, or call the CCR `/edits` API, controls it. Without\n * an origin check a crafted `https://evil.example/beacon.png` turns every\n * published render into a visitor-IP exfil beacon (and an SSRF under\n * server-fetching `next/image`). getMediaProps therefore rejects any url that\n * is not `https` on this exact origin. If `mediaBaseUrl` is omitted it\n * **fails closed** (empty src) rather than degrading to an insecure pass-through.\n *\n * Local dev exception: when the configured base is itself `http` on a\n * loopback host (`localhost`, `127.0.0.1`, `[::1]` — a local `wrangler dev`\n * worker), same-origin `http` urls are allowed so rich values render locally.\n */\n mediaBaseUrl?: string;\n /** Transform params merged onto the validated URL (width, height, format, quality). */\n transform?: ImageTransformParams;\n}\n\n// Hosts where an http mediaBaseUrl is honored (local `wrangler dev`). Exact\n// hostnames only — no *.localhost subdomains.\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\n\n/**\n * Returns the url only if it is on the configured CDN origin AND https —\n * except that an http origin is honored when the configured mediaBaseUrl is\n * itself http on a loopback host (local dev against `wrangler dev`, which\n * serves plain http). Production is unaffected: real CDN bases are https, so\n * every http url still fails the origin check. Otherwise \"\". Fails closed\n * when no origin is configured (see GetMediaPropsOptions).\n */\nfunction validateSrc(url: string, mediaBaseUrl?: string): string {\n if (!url || !mediaBaseUrl) return \"\";\n let parsed: URL;\n let base: URL;\n try {\n parsed = new URL(url);\n base = new URL(mediaBaseUrl);\n } catch {\n return \"\";\n }\n if (parsed.origin !== base.origin) return \"\";\n if (parsed.protocol !== \"https:\") {\n // Non-https is allowed only as plain http against an explicitly-configured\n // local-dev loopback base. Checking parsed.protocol too keeps schemes whose\n // origin serializes to the base origin (blob:http://…) out of the carve-out.\n const isLocalDevBase = base.protocol === \"http:\" && LOOPBACK_HOSTS.has(base.hostname);\n if (!isLocalDevBase || parsed.protocol !== \"http:\") return \"\";\n }\n return url;\n}\n\n/**\n * Normalizes a `string | MediaValue | null` into `{ src, alt, width?, height? }`.\n * A string (basic mode) yields `alt: \"\"`. `src` is validated against the CDN\n * origin (see GetMediaPropsOptions) and empty on rejection.\n */\nexport function getMediaProps(\n value: MediaFieldValue | null | undefined,\n options?: GetMediaPropsOptions,\n): MediaProps {\n const mediaBaseUrl = options?.mediaBaseUrl;\n const transform = options?.transform;\n\n const finalize = (rawUrl: string): string => {\n const validated = validateSrc(rawUrl, mediaBaseUrl);\n return validated && transform ? buildImageUrl(validated, transform) : validated;\n };\n\n if (value == null) return { src: \"\", alt: \"\" };\n\n if (typeof value === \"string\") {\n return { src: finalize(value), alt: \"\" };\n }\n\n const props: MediaProps = {\n src: finalize(typeof value.url === \"string\" ? value.url : \"\"),\n alt: typeof value.alt === \"string\" ? value.alt : \"\",\n };\n if (typeof value.width === \"number\") props.width = value.width;\n if (typeof value.height === \"number\") props.height = value.height;\n return props;\n}\n\n/**\n * Common-case `<img>` wrapper. Renders nothing when the src is rejected/empty\n * (a broken foreign-origin image is never emitted). `alt` may be overridden;\n * otherwise it comes from the value.\n */\nexport function MediaImage({\n image,\n mediaBaseUrl,\n transform,\n alt,\n ...rest\n}: {\n image: MediaFieldValue | null | undefined;\n mediaBaseUrl?: string;\n transform?: ImageTransformParams;\n} & Omit<ImgHTMLAttributes<HTMLImageElement>, \"src\">): ReactElement | null {\n const props = getMediaProps(image, { mediaBaseUrl, transform });\n if (!props.src) return null;\n return (\n <img\n src={props.src}\n alt={alt ?? props.alt}\n {...(props.width !== undefined ? { width: props.width } : {})}\n {...(props.height !== undefined ? { height: props.height } : {})}\n {...rest}\n />\n );\n}\n\n/**\n * Collects the metadata fields a figure should render, generically (req. R14):\n * with a schema, iterate its entries (canonical order/labels); without one,\n * iterate the value's own string metadata keys. Either way a new backend text\n * field appears with no plugin release. `alt` (on the `<img>`) and non-text\n * values (width/height/metaSchemaVersion) are excluded from the caption.\n */\nfunction collectFigureFields(\n image: MediaFieldValue | null | undefined,\n schema?: MetadataFieldDef[],\n): Array<{ name: string; label?: string; value: string }> {\n if (!isMediaValue(image)) return [];\n const out: Array<{ name: string; label?: string; value: string }> = [];\n if (schema && schema.length > 0) {\n for (const entry of schema) {\n if (entry.name === \"alt\") continue;\n const raw = image[entry.name];\n if (raw === undefined || raw === \"\") continue;\n out.push({ name: entry.name, label: entry.label, value: String(raw) });\n }\n } else {\n for (const [k, v] of Object.entries(image)) {\n if (k === \"alt\") continue;\n if (STRUCTURAL_MEDIA_KEYS.has(k)) continue;\n if (typeof v !== \"string\" || v === \"\") continue;\n out.push({ name: k, value: v });\n }\n }\n return out;\n}\n\n/**\n * `<figure>` that renders the image plus the schema-advertised text fields as\n * ESCAPED text (React default escaping — never dangerouslySetInnerHTML, req.\n * R6). Renders nothing when the src is rejected/empty.\n */\nexport function MediaFigure({\n image,\n schema,\n mediaBaseUrl,\n transform,\n className,\n captionClassName,\n}: {\n image: MediaFieldValue | null | undefined;\n schema?: MetadataFieldDef[];\n mediaBaseUrl?: string;\n transform?: ImageTransformParams;\n className?: string;\n captionClassName?: string;\n}): ReactElement | null {\n const props = getMediaProps(image, { mediaBaseUrl, transform });\n if (!props.src) return null;\n\n const fields = collectFigureFields(image, schema);\n\n return (\n <figure className={className}>\n <img\n src={props.src}\n alt={props.alt}\n {...(props.width !== undefined ? { width: props.width } : {})}\n {...(props.height !== undefined ? { height: props.height } : {})}\n />\n {fields.length > 0 && (\n <figcaption className={captionClassName}>\n {fields.map((f) => (\n <span key={f.name} data-field={f.name}>\n {f.value}\n </span>\n ))}\n </figcaption>\n )}\n </figure>\n );\n}\n","// Ready-made Puck component for the rich `p1-media` field. Server-safe (no\n// hooks, no context) so it can be registered in configs that render via RSC.\nimport type { CSSProperties, ReactElement, ReactNode } from \"react\";\nimport type { ComponentConfig, Field } from \"@puckeditor/core\";\nimport { getMediaProps, MediaFigure } from \"./render\";\nimport type { ImageTransformParams } from \"./utils\";\nimport type { MediaFieldValue, MetadataFieldDef } from \"./types\";\n\nexport interface MediaFigureBlockProps {\n photo: MediaFieldValue | null;\n}\n\nexport interface MediaFigureBlockOptions {\n /**\n * The CDN image origin used to validate value URLs — NOT the Worker API\n * URL. See GetMediaPropsOptions. Defaults to the production origin\n * (\"https://media.p1.pantheon.io\"); override for sandbox/staging/local dev.\n */\n mediaBaseUrl?: string;\n /**\n * Render-time transform. Include BOTH width and height — the editor's crop\n * intent only changes the output when there is a target aspect ratio.\n * Defaults to `{ width: 1200, height: 630, format: \"auto\" }`.\n */\n transform?: ImageTransformParams;\n /** Component label in the Puck sidebar. Defaults to \"Media Figure\". */\n label?: string;\n /** Label of the media field. Defaults to \"Photo\". */\n fieldLabel?: string;\n /** Metadata schema passed to MediaFigure — pins figcaption field order/labels. */\n schema?: MetadataFieldDef[];\n className?: string;\n captionClassName?: string;\n /** Rendered when no photo is chosen (or its URL fails origin validation). */\n placeholder?: ReactNode;\n}\n\nconst DEFAULT_TRANSFORM: ImageTransformParams = { width: 1200, height: 630, format: \"auto\" };\n\n/** Same production default as createMediaPlugin's workerUrl — same host serves both. */\nconst DEFAULT_MEDIA_BASE_URL = \"https://media.p1.pantheon.io\";\n\nconst placeholderStyle: CSSProperties = {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minHeight: \"200px\",\n borderRadius: \"8px\",\n backgroundColor: \"#e5e5e5\",\n color: \"#525252\",\n fontSize: \"14px\",\n};\n\n/**\n * Builds a registerable Puck component around the rich `p1-media` field and\n * `MediaFigure`. Spares consumers the custom-field-type cast and the\n * width+height transform pitfall:\n *\n * ```tsx\n * const config = {\n * components: {\n * MediaFigureBlock: createMediaFigureBlock({ mediaBaseUrl: MEDIA_BASE }),\n * },\n * };\n * ```\n */\nexport function createMediaFigureBlock(\n options: MediaFigureBlockOptions,\n): ComponentConfig<MediaFigureBlockProps> {\n const {\n mediaBaseUrl = DEFAULT_MEDIA_BASE_URL,\n transform = DEFAULT_TRANSFORM,\n label = \"Media Figure\",\n fieldLabel = \"Photo\",\n schema,\n className,\n captionClassName,\n placeholder = \"Choose a photo from the media library\",\n } = options;\n\n return {\n label,\n fields: {\n // `p1-media` is registered by createMediaPlugin via overrides.fieldTypes,\n // so it is not part of Puck's built-in Field union.\n photo: { type: \"p1-media\", label: fieldLabel } as unknown as Field,\n },\n defaultProps: {\n photo: null,\n },\n render: ({ photo }: MediaFigureBlockProps): ReactElement => {\n const { src } = getMediaProps(photo ?? null, { mediaBaseUrl });\n if (!src) {\n return <div style={placeholderStyle}>{placeholder}</div>;\n }\n return (\n <MediaFigure\n image={photo ?? null}\n mediaBaseUrl={mediaBaseUrl}\n transform={transform}\n schema={schema}\n className={className}\n captionClassName={captionClassName}\n />\n );\n },\n };\n}\n"],"mappings":"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","names":["import_jsx_runtime"]}
|