@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.
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/admin/FocalPointField.d.ts +22 -0
- package/dist/admin/FocalPointField.d.ts.map +1 -0
- package/dist/admin/FocalPointField.js +71 -0
- package/dist/admin/FocalPointField.js.map +1 -0
- package/dist/admin/MediaLibrary.d.ts +43 -0
- package/dist/admin/MediaLibrary.d.ts.map +1 -0
- package/dist/admin/MediaLibrary.js +698 -0
- package/dist/admin/MediaLibrary.js.map +1 -0
- package/dist/admin/MediaListPickerField.d.ts +14 -0
- package/dist/admin/MediaListPickerField.d.ts.map +1 -0
- package/dist/admin/MediaListPickerField.js +161 -0
- package/dist/admin/MediaListPickerField.js.map +1 -0
- package/dist/admin/MediaPickerField.d.ts +25 -0
- package/dist/admin/MediaPickerField.d.ts.map +1 -0
- package/dist/admin/MediaPickerField.js +82 -0
- package/dist/admin/MediaPickerField.js.map +1 -0
- package/dist/admin/MediaUrlField.d.ts +14 -0
- package/dist/admin/MediaUrlField.d.ts.map +1 -0
- package/dist/admin/MediaUrlField.js +29 -0
- package/dist/admin/MediaUrlField.js.map +1 -0
- package/dist/admin/index.d.ts +11 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +49 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/messages.d.ts +24 -0
- package/dist/admin/messages.d.ts.map +1 -0
- package/dist/admin/messages.js +28 -0
- package/dist/admin/messages.js.map +1 -0
- package/dist/blocks/file/index.d.ts +2 -0
- package/dist/blocks/file/index.d.ts.map +1 -0
- package/dist/blocks/file/index.js +109 -0
- package/dist/blocks/file/index.js.map +1 -0
- package/dist/blocks/file/normalize.d.ts +10 -0
- package/dist/blocks/file/normalize.d.ts.map +1 -0
- package/dist/blocks/file/normalize.js +45 -0
- package/dist/blocks/file/normalize.js.map +1 -0
- package/dist/blocks/image/index.d.ts +2 -0
- package/dist/blocks/image/index.d.ts.map +1 -0
- package/dist/blocks/image/index.js +141 -0
- package/dist/blocks/image/index.js.map +1 -0
- package/dist/blocks/image/normalize.d.ts +7 -0
- package/dist/blocks/image/normalize.d.ts.map +1 -0
- package/dist/blocks/image/normalize.js +20 -0
- package/dist/blocks/image/normalize.js.map +1 -0
- package/dist/blocks.d.ts +2 -0
- package/dist/blocks.d.ts.map +1 -0
- package/dist/blocks.js +6 -0
- package/dist/blocks.js.map +1 -0
- package/dist/builder.d.ts +80 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +61 -0
- package/dist/builder.js.map +1 -0
- package/dist/fields.d.ts +3 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +7 -0
- package/dist/fields.js.map +1 -0
- package/dist/image-dimensions.d.ts +7 -0
- package/dist/image-dimensions.d.ts.map +1 -0
- package/dist/image-dimensions.js +142 -0
- package/dist/image-dimensions.js.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +216 -0
- package/dist/index.js.map +1 -0
- package/dist/lookup.d.ts +43 -0
- package/dist/lookup.d.ts.map +1 -0
- package/dist/lookup.js +154 -0
- package/dist/lookup.js.map +1 -0
- package/dist/magic-bytes.d.ts +4 -0
- package/dist/magic-bytes.d.ts.map +1 -0
- package/dist/magic-bytes.js +107 -0
- package/dist/magic-bytes.js.map +1 -0
- package/dist/mcp-tools.d.ts +10 -0
- package/dist/mcp-tools.d.ts.map +1 -0
- package/dist/mcp-tools.js +43 -0
- package/dist/mcp-tools.js.map +1 -0
- package/dist/media-blocks.d.ts +3 -0
- package/dist/media-blocks.d.ts.map +1 -0
- package/dist/media-blocks.js +10 -0
- package/dist/media-blocks.js.map +1 -0
- package/dist/meta.d.ts +12 -0
- package/dist/meta.d.ts.map +1 -0
- package/dist/meta.js +20 -0
- package/dist/meta.js.map +1 -0
- package/dist/mime.d.ts +3 -0
- package/dist/mime.d.ts.map +1 -0
- package/dist/mime.js +58 -0
- package/dist/mime.js.map +1 -0
- package/dist/read-service.d.ts +66 -0
- package/dist/read-service.d.ts.map +1 -0
- package/dist/read-service.js +156 -0
- package/dist/read-service.js.map +1 -0
- package/dist/rpc.d.ts +7 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +352 -0
- package/dist/rpc.js.map +1 -0
- package/dist/serve-route.d.ts +19 -0
- package/dist/serve-route.d.ts.map +1 -0
- package/dist/serve-route.js +122 -0
- package/dist/serve-route.js.map +1 -0
- package/dist/upload-route.d.ts +17 -0
- package/dist/upload-route.d.ts.map +1 -0
- package/dist/upload-route.js +106 -0
- package/dist/upload-route.js.map +1 -0
- 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"}
|