@plumix/plugin-media 0.1.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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +211 -0
  3. package/dist/admin/FocalPointField.d.ts +22 -0
  4. package/dist/admin/FocalPointField.d.ts.map +1 -0
  5. package/dist/admin/FocalPointField.js +71 -0
  6. package/dist/admin/FocalPointField.js.map +1 -0
  7. package/dist/admin/MediaLibrary.d.ts +43 -0
  8. package/dist/admin/MediaLibrary.d.ts.map +1 -0
  9. package/dist/admin/MediaLibrary.js +698 -0
  10. package/dist/admin/MediaLibrary.js.map +1 -0
  11. package/dist/admin/MediaListPickerField.d.ts +14 -0
  12. package/dist/admin/MediaListPickerField.d.ts.map +1 -0
  13. package/dist/admin/MediaListPickerField.js +161 -0
  14. package/dist/admin/MediaListPickerField.js.map +1 -0
  15. package/dist/admin/MediaPickerField.d.ts +25 -0
  16. package/dist/admin/MediaPickerField.d.ts.map +1 -0
  17. package/dist/admin/MediaPickerField.js +82 -0
  18. package/dist/admin/MediaPickerField.js.map +1 -0
  19. package/dist/admin/MediaUrlField.d.ts +14 -0
  20. package/dist/admin/MediaUrlField.d.ts.map +1 -0
  21. package/dist/admin/MediaUrlField.js +29 -0
  22. package/dist/admin/MediaUrlField.js.map +1 -0
  23. package/dist/admin/index.d.ts +11 -0
  24. package/dist/admin/index.d.ts.map +1 -0
  25. package/dist/admin/index.js +49 -0
  26. package/dist/admin/index.js.map +1 -0
  27. package/dist/admin/messages.d.ts +24 -0
  28. package/dist/admin/messages.d.ts.map +1 -0
  29. package/dist/admin/messages.js +28 -0
  30. package/dist/admin/messages.js.map +1 -0
  31. package/dist/blocks/file/index.d.ts +2 -0
  32. package/dist/blocks/file/index.d.ts.map +1 -0
  33. package/dist/blocks/file/index.js +109 -0
  34. package/dist/blocks/file/index.js.map +1 -0
  35. package/dist/blocks/file/normalize.d.ts +10 -0
  36. package/dist/blocks/file/normalize.d.ts.map +1 -0
  37. package/dist/blocks/file/normalize.js +45 -0
  38. package/dist/blocks/file/normalize.js.map +1 -0
  39. package/dist/blocks/image/index.d.ts +2 -0
  40. package/dist/blocks/image/index.d.ts.map +1 -0
  41. package/dist/blocks/image/index.js +141 -0
  42. package/dist/blocks/image/index.js.map +1 -0
  43. package/dist/blocks/image/normalize.d.ts +7 -0
  44. package/dist/blocks/image/normalize.d.ts.map +1 -0
  45. package/dist/blocks/image/normalize.js +20 -0
  46. package/dist/blocks/image/normalize.js.map +1 -0
  47. package/dist/blocks.d.ts +2 -0
  48. package/dist/blocks.d.ts.map +1 -0
  49. package/dist/blocks.js +6 -0
  50. package/dist/blocks.js.map +1 -0
  51. package/dist/builder.d.ts +80 -0
  52. package/dist/builder.d.ts.map +1 -0
  53. package/dist/builder.js +61 -0
  54. package/dist/builder.js.map +1 -0
  55. package/dist/fields.d.ts +3 -0
  56. package/dist/fields.d.ts.map +1 -0
  57. package/dist/fields.js +7 -0
  58. package/dist/fields.js.map +1 -0
  59. package/dist/image-dimensions.d.ts +7 -0
  60. package/dist/image-dimensions.d.ts.map +1 -0
  61. package/dist/image-dimensions.js +142 -0
  62. package/dist/image-dimensions.js.map +1 -0
  63. package/dist/index.d.ts +62 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +216 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/lookup.d.ts +43 -0
  68. package/dist/lookup.d.ts.map +1 -0
  69. package/dist/lookup.js +154 -0
  70. package/dist/lookup.js.map +1 -0
  71. package/dist/magic-bytes.d.ts +4 -0
  72. package/dist/magic-bytes.d.ts.map +1 -0
  73. package/dist/magic-bytes.js +107 -0
  74. package/dist/magic-bytes.js.map +1 -0
  75. package/dist/mcp-tools.d.ts +10 -0
  76. package/dist/mcp-tools.d.ts.map +1 -0
  77. package/dist/mcp-tools.js +43 -0
  78. package/dist/mcp-tools.js.map +1 -0
  79. package/dist/media-blocks.d.ts +3 -0
  80. package/dist/media-blocks.d.ts.map +1 -0
  81. package/dist/media-blocks.js +10 -0
  82. package/dist/media-blocks.js.map +1 -0
  83. package/dist/meta.d.ts +12 -0
  84. package/dist/meta.d.ts.map +1 -0
  85. package/dist/meta.js +20 -0
  86. package/dist/meta.js.map +1 -0
  87. package/dist/mime.d.ts +3 -0
  88. package/dist/mime.d.ts.map +1 -0
  89. package/dist/mime.js +58 -0
  90. package/dist/mime.js.map +1 -0
  91. package/dist/read-service.d.ts +66 -0
  92. package/dist/read-service.d.ts.map +1 -0
  93. package/dist/read-service.js +156 -0
  94. package/dist/read-service.js.map +1 -0
  95. package/dist/rpc.d.ts +7 -0
  96. package/dist/rpc.d.ts.map +1 -0
  97. package/dist/rpc.js +352 -0
  98. package/dist/rpc.js.map +1 -0
  99. package/dist/serve-route.d.ts +19 -0
  100. package/dist/serve-route.d.ts.map +1 -0
  101. package/dist/serve-route.js +122 -0
  102. package/dist/serve-route.js.map +1 -0
  103. package/dist/upload-route.d.ts +17 -0
  104. package/dist/upload-route.d.ts.map +1 -0
  105. package/dist/upload-route.js +106 -0
  106. package/dist/upload-route.js.map +1 -0
  107. package/package.json +97 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Plumix Contributors
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,211 @@
1
+ # @plumix/plugin-media
2
+
3
+ Media library for Plumix. Registers a `media` entry type, RPC procedures
4
+ for uploads, and an admin Media Library page (grid + thumbnails +
5
+ drag-to-upload + delete + alt-text editing + Copy URL + infinite scroll).
6
+
7
+ Two upload modes, picked automatically based on what your runtime
8
+ exposes:
9
+
10
+ - **Presigned PUT** — bytes go straight from the browser to your bucket
11
+ via an AWS-SigV4 query-signed URL. No worker hop, no CPU cost on
12
+ upload. Requires S3 API credentials (see below) and bucket CORS rules.
13
+ - **Worker-routed** — bytes flow through the worker via the storage
14
+ binding's `put()`. Works the moment the binding is attached, no
15
+ credentials or CORS configuration. Capped at the runtime's request-
16
+ body limit (~100 MiB on Workers free).
17
+
18
+ You don't pick — `media.createUploadUrl` returns a presigned URL when
19
+ `storage.presignPut` is wired up, otherwise a `/_plumix/media/upload/<id>`
20
+ URL routed through the worker. The browser PUTs to whichever it gets.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pnpm add @plumix/plugin-media
26
+ ```
27
+
28
+ Wire it into `plumix.config.ts`. The plugin reads two slots from the
29
+ host config: `storage` (where bytes live) and `imageDelivery` (how
30
+ thumbnails are derived). The Cloudflare runtime ships compatible
31
+ implementations.
32
+
33
+ ```ts
34
+ import { auth, plumix } from "plumix";
35
+
36
+ import { media } from "@plumix/plugin-media";
37
+ import { cloudflare, d1, images, r2 } from "@plumix/runtime-cloudflare";
38
+
39
+ export default plumix({
40
+ runtime: cloudflare(),
41
+ database: d1({ binding: "DB", session: "auto" }),
42
+ storage: r2({
43
+ binding: "MEDIA",
44
+ publicUrlBase: "https://media.example.com",
45
+ s3: {
46
+ bucket: "my-media-bucket",
47
+ accountId: process.env.CF_ACCOUNT_ID!,
48
+ accessKeyId: process.env.R2_ACCESS_KEY_ID!,
49
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
50
+ },
51
+ }),
52
+ imageDelivery: images({ zone: "media.example.com" }),
53
+ auth: auth({
54
+ passkey: {/* … */},
55
+ }),
56
+ plugins: [media()],
57
+ });
58
+ ```
59
+
60
+ `wrangler.jsonc`:
61
+
62
+ ```jsonc
63
+ {
64
+ "r2_buckets": [{ "binding": "MEDIA", "bucket_name": "my-media-bucket" }],
65
+ }
66
+ ```
67
+
68
+ ## R2 CORS (only required for presigned-PUT mode)
69
+
70
+ Skip this section if you're running on the binding-only path — the
71
+ worker-routed upload is same-origin so no CORS rules are needed.
72
+
73
+ For presigned PUTs, the browser hits R2 directly. Without CORS rules on
74
+ the bucket, the request fails before the bytes ever reach R2. Apply
75
+ this once per bucket:
76
+
77
+ ```bash
78
+ wrangler r2 bucket cors put my-media-bucket --rules '[{
79
+ "AllowedOrigins": ["https://your-admin.example.com"],
80
+ "AllowedMethods": ["PUT"],
81
+ "AllowedHeaders": ["Content-Type"],
82
+ "ExposeHeaders": ["ETag"],
83
+ "MaxAgeSeconds": 3600
84
+ }]'
85
+ ```
86
+
87
+ Add every origin you serve the admin from (production, preview deploys,
88
+ `http://localhost:8787` for local dev with `wrangler dev`).
89
+
90
+ ## Image Transformations (optional)
91
+
92
+ `imageDelivery: images({ zone })` turns image URLs into Cloudflare
93
+ Image Transformations URLs (`/cdn-cgi/image/<opts>/<src>`). The zone
94
+ must be a Cloudflare zone with Image Transformations enabled in the
95
+ dashboard, fronting the same bucket.
96
+
97
+ Without `imageDelivery`, the plugin falls back to serving full-size
98
+ images directly — fine for low-traffic sites, but every grid card
99
+ fetches the original.
100
+
101
+ ## Configuration
102
+
103
+ ```ts
104
+ media({
105
+ // MIME types accepted by createUploadUrl. Defaults to
106
+ // DEFAULT_ACCEPTED_TYPES (jpeg/png/gif/svg/webp/avif/pdf/doc(x)/
107
+ // xls(x)/ppt(x)/txt/csv/md/mp3/wav/ogg/mp4/webm/mov/zip).
108
+ acceptedTypes: ["image/jpeg", "image/png"],
109
+
110
+ // Maximum upload size (bytes). Default: 25 MiB. Cloudflare Workers
111
+ // free plan caps request bodies at 100 MiB; paid plans are higher.
112
+ maxUploadSize: 10 * 1024 * 1024,
113
+ });
114
+ ```
115
+
116
+ ## Capabilities
117
+
118
+ Derived automatically from the `media` entry type:
119
+
120
+ | Cap | Min role |
121
+ | ---------------------- | ------------- |
122
+ | `entry:media:read` | `subscriber` |
123
+ | `entry:media:create` | `contributor` |
124
+ | `entry:media:edit_own` | `contributor` |
125
+ | `entry:media:edit_any` | `editor` |
126
+ | `entry:media:delete` | `editor` |
127
+ | `entry:media:publish` | `author` |
128
+
129
+ Owners can always confirm/update/delete their own media. `delete` and
130
+ `edit_any` let editors+ act on others'.
131
+
132
+ ## Upload flow
133
+
134
+ ```
135
+ client → media.createUploadUrl({ filename, contentType, size })
136
+ → server: validate, draft entry, mint upload URL
137
+ ← { uploadUrl, headers, mediaId, storageKey, expiresAt }
138
+ └─ presigned R2 URL (storage.presignPut available)
139
+ └─ /_plumix/media/upload/<id> (worker-routed fallback)
140
+
141
+ client → PUT bytes (XHR with progress)
142
+ ← 200/204 OK
143
+ └─ R2: bytes never traverse the worker
144
+ └─ Worker: bytes streamed through env.MEDIA.put()
145
+
146
+ client → media.confirm({ id: mediaId })
147
+ → server: read first 64 bytes, magic-byte sniff, flip to
148
+ published. Mismatch → object deleted + CONFLICT.
149
+ ← { id, url, mime, size, storageKey }
150
+ ```
151
+
152
+ If the PUT fails, `confirm` rejects (mime mismatch, network drop, …),
153
+ or `presignPut` itself throws after the draft row landed, both client
154
+ and server fire a best-effort `media.delete` to GC the draft.
155
+
156
+ ## RPC procedures
157
+
158
+ - `media.createUploadUrl({ filename, contentType, size })` — phase 1 of
159
+ upload. Inserts a `draft` entry, mints a presigned PUT URL.
160
+ - `media.confirm({ id })` — phase 3. Verifies bytes landed and match the
161
+ claimed mime, flips draft → published.
162
+ - `media.list({ limit, offset })` — paginated published media + URLs +
163
+ thumbnails (image/\* only).
164
+ - `media.update({ id, alt?, title? })` — owner OR `edit_any` writes
165
+ alt-text / title to `meta`.
166
+ - `media.delete({ id })` — owner OR `entry:media:delete` removes the
167
+ row + best-effort storage delete.
168
+
169
+ ## Admin UI features
170
+
171
+ - Grid of cards with image thumbnails (via `imageDelivery`) or
172
+ category glyphs for non-images.
173
+ - **Drag-and-drop** files onto the grid to upload.
174
+ - **Multi-file upload** with progress bar — concurrent uploads.
175
+ - **Inline alt-text editing** per card (blur to save).
176
+ - **Copy URL** button per card → clipboard.
177
+ - **Delete** with `window.confirm` gate.
178
+ - **Infinite scroll** — `IntersectionObserver` sentinel triggers the
179
+ next page automatically.
180
+
181
+ ## Limitations & follow-ups
182
+
183
+ The plugin doesn't (yet) ship:
184
+
185
+ - **Per-user upload quota / rate limit** — a contributor can mint
186
+ presigned URLs as fast as they can call the RPC. Track `meta.size`
187
+ per `authorId` or use a KV counter for at-scale deploys.
188
+ - **Server-side draft garbage collection** — drafts created by
189
+ `createUploadUrl` get cleaned up by both client and server on the
190
+ immediate failure paths, but a session that drops mid-flight can
191
+ still leak a row. A scheduled handler that deletes `type=media AND
192
+ status=draft AND created_at < now-1h` would close the loop.
193
+ - **Featured image picker** — no meta box for posts to pick a featured
194
+ image from the library. Wire it from the consuming plugin.
195
+ - **Bulk operations / search / tags** — single-row CRUD only. The
196
+ generic `entry.*` RPCs handle search/filter for advanced cases.
197
+ - **Real worker e2e** — the plugin's e2e mocks RPC against the admin
198
+ static dist; the playground app at
199
+ `packages/plugins/media/playground` is the manual end-to-end rig.
200
+
201
+ ## Support
202
+
203
+ Have a question? Start a [discussion](https://github.com/withplumix/plumix/discussions). Found a bug? [Open an issue](https://github.com/withplumix/plumix/issues).
204
+
205
+ ## Contributing
206
+
207
+ PRs and ideas welcome. The [Contributing guide](https://github.com/withplumix/plumix/blob/main/CONTRIBUTING.md) gets you set up — new contributors especially welcome.
208
+
209
+ ## License
210
+
211
+ [MIT](https://github.com/withplumix/plumix/blob/main/LICENSE) © Plumix Contributors
@@ -0,0 +1,22 @@
1
+ import type { ReactNode } from "react";
2
+ interface FocalPoint {
3
+ readonly x: number;
4
+ readonly y: number;
5
+ }
6
+ /** Pointer position → normalized focal point within the preview's bounds. */
7
+ export declare function focalFromPointer(rect: {
8
+ readonly left: number;
9
+ readonly top: number;
10
+ readonly width: number;
11
+ readonly height: number;
12
+ }, clientX: number, clientY: number): FocalPoint;
13
+ export declare function FocalPointField({ rhf, testId, attrs, }: {
14
+ readonly rhf: {
15
+ readonly value: unknown;
16
+ readonly onChange: (next: unknown) => void;
17
+ };
18
+ readonly testId: string;
19
+ readonly attrs?: Readonly<Record<string, unknown>>;
20
+ }): ReactNode;
21
+ export {};
22
+ //# sourceMappingURL=FocalPointField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FocalPointField.d.ts","sourceRoot":"","sources":["../../src/admin/FocalPointField.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAqC,MAAM,OAAO,CAAC;AAY1E,UAAU,UAAU;IAClB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAyBD,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE;IACJ,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,EACD,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,UAAU,CAMZ;AAED,wBAAgB,eAAe,CAAC,EAC9B,GAAG,EACH,MAAM,EACN,KAAK,GACN,EAAE;IACD,QAAQ,CAAC,GAAG,EAAE;QACZ,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QACxB,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;KAC5C,CAAC;IACF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACpD,GAAG,SAAS,CAmEZ"}
@@ -0,0 +1,71 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useRef } from "react";
3
+ import { useLingui } from "plumix/i18n";
4
+ // Reuses the media picker's "no media selected" string — the focal control is
5
+ // inert until an image is chosen — rather than adding a near-duplicate.
6
+ import { M } from "./messages.js";
7
+ const clamp01 = (n) => Math.min(1, Math.max(0, n));
8
+ function readFocalPoint(raw) {
9
+ if (raw && typeof raw === "object") {
10
+ const o = raw;
11
+ return {
12
+ x: typeof o.x === "number" ? clamp01(o.x) : 0.5,
13
+ y: typeof o.y === "number" ? clamp01(o.y) : 0.5,
14
+ };
15
+ }
16
+ return { x: 0.5, y: 0.5 };
17
+ }
18
+ // The block's image url — the picked media's url, else the raw src escape hatch.
19
+ function imageUrl(attrs) {
20
+ const media = attrs.media;
21
+ if (media && typeof media === "object") {
22
+ const url = media.url;
23
+ if (typeof url === "string" && url !== "")
24
+ return url;
25
+ }
26
+ return typeof attrs.src === "string" ? attrs.src : "";
27
+ }
28
+ /** Pointer position → normalized focal point within the preview's bounds. */
29
+ export function focalFromPointer(rect, clientX, clientY) {
30
+ if (rect.width === 0 || rect.height === 0)
31
+ return { x: 0.5, y: 0.5 };
32
+ return {
33
+ x: clamp01((clientX - rect.left) / rect.width),
34
+ y: clamp01((clientY - rect.top) / rect.height),
35
+ };
36
+ }
37
+ export function FocalPointField({ rhf, testId, attrs, }) {
38
+ const { i18n } = useLingui();
39
+ const url = imageUrl(attrs ?? {});
40
+ const focal = readFocalPoint(rhf.value);
41
+ const frameRef = useRef(null);
42
+ const draggingRef = useRef(false);
43
+ const setFromPointer = useCallback((clientX, clientY) => {
44
+ const el = frameRef.current;
45
+ if (!el)
46
+ return;
47
+ rhf.onChange(focalFromPointer(el.getBoundingClientRect(), clientX, clientY));
48
+ }, [rhf]);
49
+ if (url === "") {
50
+ return (_jsx("p", { className: "text-muted-foreground text-sm", "data-testid": `${testId}-empty`, children: i18n._(M.empty) }));
51
+ }
52
+ return (_jsxs("div", { ref: frameRef, "data-testid": testId, className: "relative inline-block cursor-crosshair overflow-hidden rounded-md border select-none", onPointerDown: (e) => {
53
+ draggingRef.current = true;
54
+ e.currentTarget.setPointerCapture(e.pointerId);
55
+ setFromPointer(e.clientX, e.clientY);
56
+ }, onPointerMove: (e) => {
57
+ if (draggingRef.current)
58
+ setFromPointer(e.clientX, e.clientY);
59
+ }, onPointerUp: () => {
60
+ draggingRef.current = false;
61
+ },
62
+ // A cancelled gesture (capture stolen, touch interrupted) never fires
63
+ // pointerup — reset here so a later move can't keep writing focal points.
64
+ onPointerCancel: () => {
65
+ draggingRef.current = false;
66
+ }, children: [_jsx("img", { src: url, alt: "", className: "pointer-events-none block max-h-48 w-auto max-w-full" }), _jsx("span", { "data-testid": `${testId}-dot`, className: "pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-black/40 shadow ring-1 ring-black/40", style: {
67
+ left: `${String(focal.x * 100)}%`,
68
+ top: `${String(focal.y * 100)}%`,
69
+ } })] }));
70
+ }
71
+ //# sourceMappingURL=FocalPointField.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FocalPointField.js","sourceRoot":"","sources":["../../src/admin/FocalPointField.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * What the picker hands back on confirm. Carries the resolved url/alt (not just
4
+ * the id) so a consumer can snapshot them without a second round-trip — the
5
+ * block editor stores these directly on the image block.
6
+ */
7
+ export interface MediaSelection {
8
+ readonly id: string;
9
+ readonly url: string;
10
+ readonly alt: string | null;
11
+ readonly mime: string;
12
+ readonly filename: string;
13
+ readonly width: number | null;
14
+ readonly height: number | null;
15
+ }
16
+ /**
17
+ * `MediaLibrary` is dual-mode: a full-page browser + uploader at
18
+ * `/media`, or an in-modal picker that emits a selection back to a
19
+ * meta-box field renderer. Page mode (default) preserves every
20
+ * existing behavior — the picker mode swaps the detail drawer for
21
+ * a footer "Use selection" CTA, accepts a hybrid card click (single
22
+ * = select, double = confirm + close), and applies a server-side
23
+ * `accept` MIME filter to the grid.
24
+ */
25
+ export interface MediaLibraryProps {
26
+ readonly mode?: "page" | "picker";
27
+ /**
28
+ * Server-side MIME filter forwarded to `media.list`. String form is
29
+ * a prefix match (`"image/"` → every `image/*` MIME); array form is
30
+ * an exact whitelist. Picker-mode only — page mode ignores it.
31
+ */
32
+ readonly accept?: string | readonly string[];
33
+ /**
34
+ * Picker-mode only: called with the bare media id (`"42"`) when the
35
+ * user confirms a selection. Single-pick semantics — caller is
36
+ * responsible for closing the modal.
37
+ */
38
+ readonly onSelect?: (selection: MediaSelection) => void;
39
+ /** Picker-mode only: called when the user clicks Cancel. */
40
+ readonly onCancel?: () => void;
41
+ }
42
+ export declare function MediaLibrary({ mode, accept, onSelect, onCancel, }?: MediaLibraryProps): ReactNode;
43
+ //# sourceMappingURL=MediaLibrary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MediaLibrary.d.ts","sourceRoot":"","sources":["../../src/admin/MediaLibrary.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AA6JlD;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AA2MD;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,cAAc,KAAK,IAAI,CAAC;IACxD,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CAChC;AAED,wBAAgB,YAAY,CAAC,EAC3B,IAAa,EACb,MAAM,EACN,QAAQ,EACR,QAAQ,GACT,GAAE,iBAAsB,GAAG,SAAS,CAwVpC"}