@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/dist/index.mjs ADDED
@@ -0,0 +1,2897 @@
1
+ "use client";
2
+ var __defProp = Object.defineProperty;
3
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
+
6
+ // src/patterns.ts
7
+ var DEFAULT_MEDIA_PATTERNS = [
8
+ /^image(?:Url)?$/,
9
+ /^logo(?:Url)?$/,
10
+ /^media(?:Url)?$/,
11
+ /^icon(?:Url)?$/,
12
+ /^thumbnail(?:Url)?$/,
13
+ /ImageUrl$/,
14
+ /LogoUrl$/
15
+ ];
16
+
17
+ // src/components/media-field.tsx
18
+ import { useState as useState5 } from "react";
19
+
20
+ // src/components/media-library.tsx
21
+ import { useState as useState4, useEffect as useEffect4, useRef as useRef4, useCallback } from "react";
22
+
23
+ // src/context.tsx
24
+ import { createContext, useContext } from "react";
25
+ import { jsx } from "react/jsx-runtime";
26
+ var MediaConfigContext = createContext(null);
27
+ function MediaConfigProvider({
28
+ config,
29
+ children
30
+ }) {
31
+ return /* @__PURE__ */ jsx(MediaConfigContext.Provider, { value: config, children });
32
+ }
33
+ function useMediaConfig() {
34
+ const ctx = useContext(MediaConfigContext);
35
+ if (!ctx)
36
+ throw new Error("useMediaConfig must be used within MediaConfigProvider");
37
+ return ctx;
38
+ }
39
+
40
+ // src/components/use-media-schema.ts
41
+ import { useState, useEffect } from "react";
42
+ var DEFAULT_METADATA_FIELDS = [
43
+ { name: "alt", label: "Alt text", type: "string" }
44
+ ];
45
+ function orderAltFirst(schema) {
46
+ const alt = schema.find((f2) => f2.name === "alt");
47
+ if (!alt) return schema;
48
+ return [alt, ...schema.filter((f2) => f2.name !== "alt")];
49
+ }
50
+ function useMediaSchema() {
51
+ const config = useMediaConfig();
52
+ const fallback = config.metadataFields && config.metadataFields.length > 0 ? config.metadataFields : DEFAULT_METADATA_FIELDS;
53
+ const [schema, setSchema] = useState(fallback);
54
+ useEffect(() => {
55
+ let cancelled = false;
56
+ (async () => {
57
+ try {
58
+ const token = await config.getAuthToken();
59
+ const headers = token ? { Authorization: "Bearer " + token } : {};
60
+ const res = await fetch(config.workerUrl + "/media/schema", { headers });
61
+ if (!res.ok) return;
62
+ const data = await res.json();
63
+ if (!cancelled && Array.isArray(data) && data.length > 0) {
64
+ setSchema(data);
65
+ }
66
+ } catch {
67
+ }
68
+ })();
69
+ return () => {
70
+ cancelled = true;
71
+ };
72
+ }, [config]);
73
+ return schema;
74
+ }
75
+
76
+ // src/components/staging.ts
77
+ function buildPatchBody(fields, row) {
78
+ const body = {};
79
+ fields.forEach((f2, c2) => {
80
+ const value = row[c2]?.trim();
81
+ body[f2.name] = value ? value : null;
82
+ });
83
+ return body;
84
+ }
85
+ function keepIncompleteRows(rows, values, progress, statuses) {
86
+ const kept = [];
87
+ const keptValues = [];
88
+ const keptProgress = [];
89
+ const keptStatuses = [];
90
+ rows.forEach((row, i2) => {
91
+ if (statuses[i2]?.step === "done") return;
92
+ kept.push(row);
93
+ keptValues.push(values[i2] ?? []);
94
+ keptProgress.push(progress[i2] ?? {});
95
+ keptStatuses.push(statuses[i2] ?? { step: "staged" });
96
+ });
97
+ return { rows: kept, values: keptValues, progress: keptProgress, statuses: keptStatuses };
98
+ }
99
+
100
+ // src/components/media-item.ts
101
+ function normalizeItem(raw) {
102
+ let metadata = raw.metadata;
103
+ if (typeof metadata === "string") {
104
+ try {
105
+ metadata = JSON.parse(metadata);
106
+ } catch {
107
+ metadata = void 0;
108
+ }
109
+ }
110
+ return {
111
+ assetId: raw.assetId,
112
+ versionId: raw.versionId,
113
+ url: String(raw.url ?? ""),
114
+ filename: String(raw.filename ?? ""),
115
+ contentType: raw.contentType,
116
+ size: raw.size,
117
+ width: raw.width,
118
+ height: raw.height,
119
+ metadata: metadata && typeof metadata === "object" ? metadata : void 0,
120
+ metaSchemaVersion: raw.metaSchemaVersion,
121
+ createdAt: raw.createdAt
122
+ };
123
+ }
124
+ function parseMediaList(data) {
125
+ if (!Array.isArray(data)) return [];
126
+ return data.map((r2) => normalizeItem(r2));
127
+ }
128
+
129
+ // src/components/upload-flow.ts
130
+ var STAGED_STATUS = { step: "staged" };
131
+ var UploadFlowError = class extends Error {
132
+ constructor(message, progress) {
133
+ super(message);
134
+ this.name = "UploadFlowError";
135
+ this.progress = progress;
136
+ }
137
+ };
138
+ function targetParams(target) {
139
+ const params = new URLSearchParams({ siteId: target.siteId });
140
+ if (target.workstreamId) params.set("workstreamId", target.workstreamId);
141
+ return params.toString();
142
+ }
143
+ async function postJson(target, path, body) {
144
+ return fetch(`${target.workerUrl}${path}?${targetParams(target)}`, {
145
+ method: "POST",
146
+ body: JSON.stringify(body),
147
+ headers: { ...await target.getAuthHeaders(), "Content-Type": "application/json" }
148
+ });
149
+ }
150
+ async function runUpload(target, file, metadata, assetId, progress, onStep) {
151
+ let presigned = progress.presigned;
152
+ let uploaded = progress.uploaded ?? false;
153
+ if (!uploaded) {
154
+ onStep("presigning");
155
+ const presignPath = assetId ? `/media/${encodeURIComponent(assetId)}/versions/presign` : "/media/presign";
156
+ let presignResponse;
157
+ try {
158
+ presignResponse = await postJson(target, presignPath, {
159
+ filename: file.name,
160
+ contentType: file.type,
161
+ size: file.size,
162
+ metadata
163
+ });
164
+ } catch (err) {
165
+ throw new UploadFlowError(`Could not reach the server: ${err.message}`, {});
166
+ }
167
+ if (!presignResponse.ok) throw new UploadFlowError(`Presign failed (${presignResponse.status})`, {});
168
+ const freshlyPresigned = await presignResponse.json();
169
+ presigned = freshlyPresigned;
170
+ onStep("uploading");
171
+ let putResponse;
172
+ try {
173
+ putResponse = await fetch(freshlyPresigned.uploadUrl, {
174
+ method: "PUT",
175
+ body: file,
176
+ headers: { "Content-Type": file.type }
177
+ });
178
+ } catch (err) {
179
+ throw new UploadFlowError(`Upload to storage failed: ${err.message}`, {});
180
+ }
181
+ if (!putResponse.ok) throw new UploadFlowError(`Upload to storage failed (${putResponse.status})`, {});
182
+ uploaded = true;
183
+ }
184
+ const nextProgress = { presigned, uploaded };
185
+ const presignedResult = presigned;
186
+ onStep("finalizing");
187
+ const finalizePath = assetId ? `/media/${encodeURIComponent(assetId)}/versions/finalize` : "/media/finalize";
188
+ const finalizeBody = assetId ? { versionId: presignedResult.versionId, filename: presignedResult.filename } : {
189
+ assetId: presignedResult.assetId,
190
+ versionId: presignedResult.versionId,
191
+ filename: presignedResult.filename,
192
+ metadata
193
+ };
194
+ let finalizeResponse;
195
+ try {
196
+ finalizeResponse = await postJson(target, finalizePath, finalizeBody);
197
+ } catch (err) {
198
+ throw new UploadFlowError(`Could not reach the server: ${err.message}`, nextProgress);
199
+ }
200
+ if (!finalizeResponse.ok) throw new UploadFlowError(`Finalize failed (${finalizeResponse.status})`, nextProgress);
201
+ const item = normalizeItem(await finalizeResponse.json());
202
+ return { item, progress: nextProgress };
203
+ }
204
+
205
+ // src/components/media-upload-dropzone.tsx
206
+ import { useRef, useState as useState2 } from "react";
207
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
208
+ function MediaUploadDropzone(props) {
209
+ const { uploading, onFilesSelected } = props;
210
+ const [dragActive, setDragActive] = useState2(false);
211
+ const fileInputRef = useRef(null);
212
+ const handleDragOver = (e2) => {
213
+ e2.preventDefault();
214
+ e2.stopPropagation();
215
+ setDragActive(true);
216
+ };
217
+ const handleDragLeave = (e2) => {
218
+ e2.preventDefault();
219
+ e2.stopPropagation();
220
+ setDragActive(false);
221
+ };
222
+ const handleDrop = (e2) => {
223
+ e2.preventDefault();
224
+ e2.stopPropagation();
225
+ setDragActive(false);
226
+ onFilesSelected(e2.dataTransfer.files);
227
+ };
228
+ return /* @__PURE__ */ jsxs(
229
+ "div",
230
+ {
231
+ style: {
232
+ border: dragActive ? "2px solid #2563eb" : "2px dashed #d0d0d0",
233
+ borderRadius: "8px",
234
+ padding: "32px",
235
+ textAlign: "center",
236
+ marginBottom: "16px",
237
+ cursor: "pointer",
238
+ backgroundColor: dragActive ? "#eff6ff" : "#fafafa",
239
+ transition: "all 0.2s"
240
+ },
241
+ onClick: () => fileInputRef.current?.click(),
242
+ onDragOver: handleDragOver,
243
+ onDragLeave: handleDragLeave,
244
+ onDrop: handleDrop,
245
+ children: [
246
+ /* @__PURE__ */ jsx2(
247
+ "input",
248
+ {
249
+ ref: fileInputRef,
250
+ type: "file",
251
+ accept: "image/*",
252
+ multiple: true,
253
+ style: { display: "none" },
254
+ onChange: (e2) => onFilesSelected(e2.target.files)
255
+ }
256
+ ),
257
+ /* @__PURE__ */ jsx2("div", { style: { fontSize: "28px", marginBottom: "8px", color: "#999" }, children: uploading ? "\u23F3" : "\u2191" }),
258
+ /* @__PURE__ */ jsx2("div", { style: { color: "#666", fontSize: "14px" }, children: uploading ? "Uploading..." : "Drag & drop files here or click to browse" })
259
+ ]
260
+ }
261
+ );
262
+ }
263
+
264
+ // src/components/metadata-grid.tsx
265
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
266
+
267
+ // src/delimited.ts
268
+ function parseWithDelimiter(text, delim) {
269
+ const rows = [];
270
+ let row = [];
271
+ let cell = "";
272
+ let inQuotes = false;
273
+ for (let i2 = 0; i2 < text.length; i2++) {
274
+ const ch = text[i2];
275
+ if (inQuotes) {
276
+ if (ch === '"') {
277
+ if (text[i2 + 1] === '"') {
278
+ cell += '"';
279
+ i2++;
280
+ } else {
281
+ inQuotes = false;
282
+ }
283
+ } else {
284
+ cell += ch;
285
+ }
286
+ } else if (ch === '"' && cell === "") {
287
+ inQuotes = true;
288
+ } else if (ch === delim) {
289
+ row.push(cell);
290
+ cell = "";
291
+ } else if (ch === "\n" || ch === "\r") {
292
+ if (ch === "\r" && text[i2 + 1] === "\n") i2++;
293
+ row.push(cell);
294
+ cell = "";
295
+ rows.push(row);
296
+ row = [];
297
+ } else {
298
+ cell += ch;
299
+ }
300
+ }
301
+ if (cell !== "" || row.length > 0) {
302
+ row.push(cell);
303
+ rows.push(row);
304
+ }
305
+ while (rows.length > 0 && rows[rows.length - 1].every((c2) => c2 === "")) rows.pop();
306
+ return rows;
307
+ }
308
+ function parseDelimited(text) {
309
+ if (!text) return [];
310
+ return parseWithDelimiter(text, text.includes(" ") ? " " : ",");
311
+ }
312
+ function norm(s2) {
313
+ return s2.trim().toLowerCase();
314
+ }
315
+ var FILENAME_HEADERS = /* @__PURE__ */ new Set(["filename", "file", "name"]);
316
+ function detectHeader(row, fields) {
317
+ let matched = 0;
318
+ const mapping = row.map(() => -1);
319
+ for (let c2 = 0; c2 < row.length; c2++) {
320
+ const cell = norm(row[c2]);
321
+ if (cell === "") continue;
322
+ if (FILENAME_HEADERS.has(cell)) {
323
+ mapping[c2] = "filename";
324
+ matched++;
325
+ continue;
326
+ }
327
+ const idx = fields.findIndex((f2) => norm(f2.name) === cell || norm(f2.label) === cell);
328
+ if (idx === -1) return null;
329
+ mapping[c2] = idx;
330
+ matched++;
331
+ }
332
+ return matched > 0 ? mapping : null;
333
+ }
334
+ function applyDelimited(opts) {
335
+ const { text, fields, rowLabels, current, anchorRow, anchorCol } = opts;
336
+ const parsed = parseDelimited(text);
337
+ if (parsed.length === 0) return null;
338
+ const multiCell = parsed.length > 1 || parsed[0].length > 1 && text.includes(" ");
339
+ if (!multiCell) return null;
340
+ const next = current.map((r2) => [...r2]);
341
+ const header = detectHeader(parsed[0], fields);
342
+ if (header && parsed.length > 1) {
343
+ const dataRows = parsed.slice(1);
344
+ const filenameCol = header.indexOf("filename");
345
+ for (let i2 = 0; i2 < dataRows.length; i2++) {
346
+ let targetRow;
347
+ if (filenameCol !== -1) {
348
+ const wanted = norm(dataRows[i2][filenameCol] ?? "");
349
+ targetRow = rowLabels.findIndex((l2) => norm(l2) === wanted);
350
+ if (targetRow === -1) continue;
351
+ } else {
352
+ targetRow = i2;
353
+ if (targetRow >= next.length) break;
354
+ }
355
+ for (let c2 = 0; c2 < dataRows[i2].length; c2++) {
356
+ const fieldIdx = header[c2];
357
+ if (typeof fieldIdx !== "number" || fieldIdx < 0) continue;
358
+ next[targetRow][fieldIdx] = dataRows[i2][c2] ?? "";
359
+ }
360
+ }
361
+ return next;
362
+ }
363
+ for (let r2 = 0; r2 < parsed.length; r2++) {
364
+ const targetRow = anchorRow + r2;
365
+ if (targetRow >= next.length) break;
366
+ for (let c2 = 0; c2 < parsed[r2].length; c2++) {
367
+ const targetCol = anchorCol + c2;
368
+ if (targetCol >= fields.length) break;
369
+ next[targetRow][targetCol] = parsed[r2][c2];
370
+ }
371
+ }
372
+ return next;
373
+ }
374
+
375
+ // src/components/metadata-grid.tsx
376
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
377
+ function MetadataGrid(props) {
378
+ const { fields, rows, values, onChange, disabled, autoFocus } = props;
379
+ const inputRefs = useRef2([]);
380
+ inputRefs.current = rows.map((_, r2) => inputRefs.current[r2] ?? []);
381
+ useEffect2(() => {
382
+ if (autoFocus) inputRefs.current[0]?.[0]?.focus();
383
+ }, []);
384
+ const setCell = (r2, c2, v) => {
385
+ const next = values.map((row) => [...row]);
386
+ next[r2][c2] = v;
387
+ onChange(next);
388
+ };
389
+ const focusCell = (r2, c2) => {
390
+ inputRefs.current[r2]?.[c2]?.focus();
391
+ };
392
+ const handleKeyDown = (e2, r2, c2) => {
393
+ if (e2.key === "Enter" || e2.key === "ArrowDown") {
394
+ e2.preventDefault();
395
+ focusCell(Math.min(r2 + 1, rows.length - 1), c2);
396
+ } else if (e2.key === "ArrowUp") {
397
+ e2.preventDefault();
398
+ focusCell(Math.max(r2 - 1, 0), c2);
399
+ }
400
+ };
401
+ const handlePaste = (e2, r2, c2) => {
402
+ const text = e2.clipboardData.getData("text/plain");
403
+ const next = applyDelimited({
404
+ text,
405
+ fields,
406
+ rowLabels: rows.map((row) => row.label),
407
+ current: values,
408
+ anchorRow: r2,
409
+ anchorCol: c2
410
+ });
411
+ if (next) {
412
+ e2.preventDefault();
413
+ onChange(next);
414
+ }
415
+ };
416
+ const cellInputStyle = {
417
+ width: "100%",
418
+ minWidth: "110px",
419
+ padding: "5px 7px",
420
+ border: "1px solid #d0d0d0",
421
+ borderRadius: "4px",
422
+ fontSize: "13px",
423
+ fontFamily: "inherit",
424
+ boxSizing: "border-box",
425
+ outline: "none"
426
+ };
427
+ const thStyle = {
428
+ textAlign: "left",
429
+ fontSize: "11px",
430
+ fontWeight: 600,
431
+ color: "#666",
432
+ padding: "4px 6px",
433
+ whiteSpace: "nowrap"
434
+ };
435
+ return /* @__PURE__ */ jsx3("div", { style: { overflowX: "auto" }, children: /* @__PURE__ */ jsxs2(
436
+ "table",
437
+ {
438
+ role: "grid",
439
+ "aria-label": "Image metadata",
440
+ style: { borderCollapse: "separate", borderSpacing: "4px", width: "100%" },
441
+ children: [
442
+ /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsxs2("tr", { children: [
443
+ /* @__PURE__ */ jsx3("th", { style: thStyle, scope: "col", children: "File" }),
444
+ fields.map((f2) => /* @__PURE__ */ jsxs2("th", { style: thStyle, scope: "col", children: [
445
+ f2.label,
446
+ f2.required ? " *" : ""
447
+ ] }, f2.name))
448
+ ] }) }),
449
+ /* @__PURE__ */ jsx3("tbody", { children: rows.map((row, r2) => /* @__PURE__ */ jsxs2("tr", { children: [
450
+ /* @__PURE__ */ jsx3(
451
+ "th",
452
+ {
453
+ scope: "row",
454
+ style: {
455
+ ...thStyle,
456
+ fontWeight: 500,
457
+ color: "#444",
458
+ maxWidth: "180px"
459
+ },
460
+ children: /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: "6px" }, children: [
461
+ row.thumbnailUrl && /* @__PURE__ */ jsx3(
462
+ "img",
463
+ {
464
+ src: row.thumbnailUrl,
465
+ alt: "",
466
+ style: {
467
+ width: "32px",
468
+ height: "32px",
469
+ objectFit: "cover",
470
+ borderRadius: "4px",
471
+ flexShrink: 0
472
+ }
473
+ }
474
+ ),
475
+ /* @__PURE__ */ jsxs2("div", { style: { overflow: "hidden" }, children: [
476
+ /* @__PURE__ */ jsx3(
477
+ "span",
478
+ {
479
+ title: row.label,
480
+ style: {
481
+ display: "block",
482
+ overflow: "hidden",
483
+ textOverflow: "ellipsis",
484
+ whiteSpace: "nowrap",
485
+ maxWidth: "140px"
486
+ },
487
+ children: row.label
488
+ }
489
+ ),
490
+ row.status && /* @__PURE__ */ jsx3(
491
+ "span",
492
+ {
493
+ style: {
494
+ display: "block",
495
+ fontSize: "11px",
496
+ fontWeight: 400,
497
+ color: row.status.isError ? "#c0392b" : "#888"
498
+ },
499
+ children: row.status.text
500
+ }
501
+ )
502
+ ] })
503
+ ] })
504
+ }
505
+ ),
506
+ fields.map((f2, c2) => /* @__PURE__ */ jsx3("td", { style: { padding: 0 }, children: /* @__PURE__ */ jsx3(
507
+ "input",
508
+ {
509
+ ref: (el) => {
510
+ inputRefs.current[r2][c2] = el;
511
+ },
512
+ type: "text",
513
+ value: values[r2]?.[c2] ?? "",
514
+ disabled,
515
+ "aria-label": `${f2.label} for ${row.label}`,
516
+ onChange: (e2) => setCell(r2, c2, e2.target.value),
517
+ onKeyDown: (e2) => handleKeyDown(e2, r2, c2),
518
+ onPaste: (e2) => handlePaste(e2, r2, c2),
519
+ style: { ...cellInputStyle, opacity: disabled ? 0.6 : 1 }
520
+ }
521
+ ) }, f2.name))
522
+ ] }, row.key)) })
523
+ ]
524
+ }
525
+ ) });
526
+ }
527
+
528
+ // src/components/media-panel-styles.ts
529
+ function primaryBtnStyle(uploading) {
530
+ return {
531
+ padding: "8px 16px",
532
+ backgroundColor: "#2563eb",
533
+ color: "white",
534
+ border: "none",
535
+ borderRadius: "6px",
536
+ fontSize: "13px",
537
+ fontWeight: 500,
538
+ cursor: uploading ? "not-allowed" : "pointer",
539
+ opacity: uploading ? 0.6 : 1,
540
+ fontFamily: "inherit"
541
+ };
542
+ }
543
+ function secondaryBtnStyle(uploading) {
544
+ return {
545
+ padding: "8px 16px",
546
+ backgroundColor: "white",
547
+ color: "#666",
548
+ border: "1px solid #d0d0d0",
549
+ borderRadius: "6px",
550
+ fontSize: "13px",
551
+ fontWeight: 500,
552
+ cursor: uploading ? "not-allowed" : "pointer",
553
+ fontFamily: "inherit"
554
+ };
555
+ }
556
+ var panelErrorStyle = {
557
+ color: "#b91c1c",
558
+ fontSize: "12px",
559
+ marginTop: "8px"
560
+ };
561
+ var panelHeadingStyle = {
562
+ fontSize: "15px",
563
+ fontWeight: 600,
564
+ color: "#333",
565
+ marginBottom: "4px"
566
+ };
567
+ var panelButtonRowStyle = {
568
+ display: "flex",
569
+ justifyContent: "flex-end",
570
+ gap: "8px",
571
+ marginTop: "16px"
572
+ };
573
+
574
+ // src/components/media-upload-panel.tsx
575
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
576
+ var STEP_LABEL = {
577
+ staged: null,
578
+ presigning: "Preparing\u2026",
579
+ uploading: "Uploading\u2026",
580
+ finalizing: "Finalizing\u2026",
581
+ done: "Done",
582
+ failed: null
583
+ // uses the row's own error message instead
584
+ };
585
+ function statusBadge(status) {
586
+ if (!status) return void 0;
587
+ if (status.step === "failed") return { text: status.error ?? "Failed", isError: true };
588
+ const text = STEP_LABEL[status.step];
589
+ return text ? { text } : void 0;
590
+ }
591
+ function MediaUploadPanel(props) {
592
+ const { schema, pending, pendingValues, onValuesChange, pendingStatus, uploading, panelError, onCancel, onUpload } = props;
593
+ return /* @__PURE__ */ jsxs3("div", { children: [
594
+ /* @__PURE__ */ jsxs3("div", { style: panelHeadingStyle, children: [
595
+ "Add details for ",
596
+ pending.length,
597
+ " ",
598
+ pending.length === 1 ? "image" : "images"
599
+ ] }),
600
+ /* @__PURE__ */ jsx4("div", { style: { fontSize: "12px", color: "#888", marginBottom: "12px" }, children: "Tab, Enter and the arrow keys move between cells. Paste TSV or CSV from a spreadsheet to fill many rows at once \u2014 include a header row (alt, caption, \u2026) plus an optional filename column to map values automatically." }),
601
+ /* @__PURE__ */ jsx4(
602
+ MetadataGrid,
603
+ {
604
+ fields: schema,
605
+ rows: pending.map((p2, i2) => ({
606
+ key: i2 + "-" + p2.file.name,
607
+ label: p2.file.name,
608
+ thumbnailUrl: p2.previewUrl,
609
+ status: statusBadge(pendingStatus[i2])
610
+ })),
611
+ values: pendingValues,
612
+ onChange: onValuesChange,
613
+ disabled: uploading,
614
+ autoFocus: true
615
+ }
616
+ ),
617
+ panelError && /* @__PURE__ */ jsx4("div", { style: panelErrorStyle, children: panelError }),
618
+ /* @__PURE__ */ jsxs3("div", { style: panelButtonRowStyle, children: [
619
+ /* @__PURE__ */ jsx4("button", { type: "button", onClick: onCancel, disabled: uploading, style: secondaryBtnStyle(uploading), children: "Cancel" }),
620
+ /* @__PURE__ */ jsx4("button", { type: "button", onClick: onUpload, disabled: uploading, style: primaryBtnStyle(uploading), children: uploading ? "Uploading\u2026" : `Upload ${pending.length} ${pending.length === 1 ? "image" : "images"}` })
621
+ ] })
622
+ ] });
623
+ }
624
+
625
+ // src/components/media-edit-panel.tsx
626
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
627
+ function MediaEditPanel(props) {
628
+ const {
629
+ editing,
630
+ editFields,
631
+ editValues,
632
+ onValuesChange,
633
+ uploading,
634
+ panelError,
635
+ onCancel,
636
+ onSave,
637
+ onReplaceImage,
638
+ replaceInputRef
639
+ } = props;
640
+ return /* @__PURE__ */ jsxs4("div", { children: [
641
+ /* @__PURE__ */ jsxs4("div", { style: { ...panelHeadingStyle, marginBottom: "12px" }, children: [
642
+ "Edit details \u2014 ",
643
+ editing.filename
644
+ ] }),
645
+ /* @__PURE__ */ jsx5(
646
+ MetadataGrid,
647
+ {
648
+ fields: editFields,
649
+ rows: [
650
+ {
651
+ key: editing.assetId ?? editing.url,
652
+ label: editing.filename,
653
+ thumbnailUrl: editing.url
654
+ }
655
+ ],
656
+ values: editValues,
657
+ onChange: onValuesChange,
658
+ disabled: uploading,
659
+ autoFocus: true
660
+ }
661
+ ),
662
+ panelError && /* @__PURE__ */ jsx5("div", { style: panelErrorStyle, children: panelError }),
663
+ /* @__PURE__ */ jsxs4("div", { style: panelButtonRowStyle, children: [
664
+ /* @__PURE__ */ jsx5(
665
+ "input",
666
+ {
667
+ ref: replaceInputRef,
668
+ type: "file",
669
+ accept: "image/*",
670
+ style: { display: "none" },
671
+ onChange: (e2) => onReplaceImage(e2.target.files?.[0])
672
+ }
673
+ ),
674
+ /* @__PURE__ */ jsx5(
675
+ "button",
676
+ {
677
+ type: "button",
678
+ onClick: () => replaceInputRef.current?.click(),
679
+ disabled: uploading,
680
+ title: "Upload a new image as a new version of this asset",
681
+ style: { ...secondaryBtnStyle(uploading), marginRight: "auto" },
682
+ children: "Replace image\u2026"
683
+ }
684
+ ),
685
+ /* @__PURE__ */ jsx5("button", { type: "button", onClick: onCancel, disabled: uploading, style: secondaryBtnStyle(uploading), children: "Cancel" }),
686
+ /* @__PURE__ */ jsx5("button", { type: "button", onClick: onSave, disabled: uploading, style: primaryBtnStyle(uploading), children: uploading ? "Saving\u2026" : "Save details" })
687
+ ] })
688
+ ] });
689
+ }
690
+
691
+ // src/components/media-grid.tsx
692
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
693
+ import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
694
+ var TILE_BATCH = 60;
695
+ function MediaGrid(props) {
696
+ const {
697
+ media,
698
+ loading,
699
+ searchQuery,
700
+ onSearchChange,
701
+ onSelectItem,
702
+ onStartEdit,
703
+ onDeleteItem,
704
+ scrollRef,
705
+ focusIdx,
706
+ onFocusIdxChange
707
+ } = props;
708
+ const [visibleCount, setVisibleCount] = useState3(TILE_BATCH);
709
+ const [confirmDeleteId, setConfirmDeleteId] = useState3(null);
710
+ const sentinelRef = useRef3(null);
711
+ const tileRefs = useRef3([]);
712
+ useEffect3(() => {
713
+ setVisibleCount(TILE_BATCH);
714
+ }, [media]);
715
+ useEffect3(() => {
716
+ const sentinel = sentinelRef.current;
717
+ if (!sentinel || typeof IntersectionObserver === "undefined") return;
718
+ const observer = new IntersectionObserver(
719
+ (entries) => {
720
+ if (entries.some((entry) => entry.isIntersecting)) {
721
+ setVisibleCount((count) => count + TILE_BATCH);
722
+ }
723
+ },
724
+ { root: scrollRef.current, rootMargin: "200px" }
725
+ );
726
+ observer.observe(sentinel);
727
+ return () => observer.disconnect();
728
+ }, [media.length, visibleCount, scrollRef]);
729
+ const computeColumns = () => {
730
+ const tiles = tileRefs.current.filter((t2) => t2 !== null);
731
+ if (tiles.length <= 1) return 1;
732
+ const firstTop = tiles[0].offsetTop;
733
+ let columns = 0;
734
+ for (const tile of tiles) {
735
+ if (tile.offsetTop !== firstTop) break;
736
+ columns++;
737
+ }
738
+ return Math.max(1, columns);
739
+ };
740
+ const focusTile = (index) => {
741
+ onFocusIdxChange(index);
742
+ tileRefs.current[index]?.focus();
743
+ };
744
+ const handleTileKeyDown = (e2, index, item) => {
745
+ const rendered = Math.min(media.length, visibleCount);
746
+ const columns = computeColumns();
747
+ let next = null;
748
+ switch (e2.key) {
749
+ case "ArrowRight":
750
+ next = Math.min(index + 1, rendered - 1);
751
+ break;
752
+ case "ArrowLeft":
753
+ next = Math.max(index - 1, 0);
754
+ break;
755
+ case "ArrowDown":
756
+ next = Math.min(index + columns, rendered - 1);
757
+ break;
758
+ case "ArrowUp":
759
+ next = Math.max(index - columns, 0);
760
+ break;
761
+ case "Home":
762
+ next = 0;
763
+ break;
764
+ case "End":
765
+ next = rendered - 1;
766
+ break;
767
+ case "Enter":
768
+ case " ":
769
+ e2.preventDefault();
770
+ onSelectItem(item);
771
+ return;
772
+ case "e":
773
+ case "E":
774
+ if (item.assetId) {
775
+ e2.preventDefault();
776
+ onStartEdit(item);
777
+ }
778
+ return;
779
+ case "Delete":
780
+ case "Backspace":
781
+ if (item.assetId) {
782
+ e2.preventDefault();
783
+ setConfirmDeleteId(item.assetId);
784
+ }
785
+ return;
786
+ case "Escape":
787
+ if (confirmDeleteId) {
788
+ e2.preventDefault();
789
+ setConfirmDeleteId(null);
790
+ }
791
+ return;
792
+ default:
793
+ return;
794
+ }
795
+ e2.preventDefault();
796
+ focusTile(next);
797
+ };
798
+ return /* @__PURE__ */ jsxs5(Fragment, { children: [
799
+ /* @__PURE__ */ jsx6(
800
+ "input",
801
+ {
802
+ type: "text",
803
+ placeholder: "Search by filename or metadata...",
804
+ value: searchQuery,
805
+ onChange: (e2) => onSearchChange(e2.target.value),
806
+ style: {
807
+ width: "100%",
808
+ padding: "10px 12px",
809
+ border: "1px solid #e0e0e0",
810
+ borderRadius: "6px",
811
+ fontSize: "14px",
812
+ fontFamily: "inherit",
813
+ outline: "none",
814
+ boxSizing: "border-box",
815
+ marginBottom: "16px"
816
+ }
817
+ }
818
+ ),
819
+ loading ? /* @__PURE__ */ jsx6("div", { style: { textAlign: "center", padding: "40px", color: "#999", fontSize: "14px" }, children: "Loading..." }) : media.length === 0 ? /* @__PURE__ */ jsx6("div", { style: { textAlign: "center", padding: "40px", color: "#999", fontSize: "14px" }, children: "No media found" }) : /* @__PURE__ */ jsx6(
820
+ "div",
821
+ {
822
+ role: "listbox",
823
+ "aria-label": "Media items \u2014 arrow keys to navigate, Enter to select, E to edit, Delete to remove",
824
+ style: {
825
+ display: "grid",
826
+ gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))",
827
+ gap: "12px"
828
+ },
829
+ children: media.slice(0, visibleCount).map((item, i2) => /* @__PURE__ */ jsxs5(
830
+ "div",
831
+ {
832
+ ref: (el) => {
833
+ tileRefs.current[i2] = el;
834
+ },
835
+ role: "option",
836
+ "aria-selected": false,
837
+ "aria-label": item.filename,
838
+ tabIndex: i2 === Math.min(focusIdx, Math.min(media.length, visibleCount) - 1) ? 0 : -1,
839
+ style: {
840
+ position: "relative",
841
+ cursor: "pointer",
842
+ border: "2px solid transparent",
843
+ borderRadius: "8px",
844
+ overflow: "hidden",
845
+ transition: "border-color 0.15s",
846
+ backgroundColor: "#f9f9f9",
847
+ outline: "none"
848
+ },
849
+ onClick: () => onSelectItem(item),
850
+ onKeyDown: (e2) => handleTileKeyDown(e2, i2, item),
851
+ onFocus: (e2) => {
852
+ onFocusIdxChange(i2);
853
+ e2.currentTarget.style.borderColor = "#2563eb";
854
+ e2.currentTarget.style.boxShadow = "0 0 0 2px rgba(37, 99, 235, 0.35)";
855
+ },
856
+ onBlur: (e2) => {
857
+ e2.currentTarget.style.borderColor = "transparent";
858
+ e2.currentTarget.style.boxShadow = "none";
859
+ },
860
+ onMouseEnter: (e2) => {
861
+ e2.currentTarget.style.borderColor = "#2563eb";
862
+ e2.currentTarget.querySelectorAll("[data-delete-btn], [data-edit-btn]").forEach((btn) => btn.style.opacity = "1");
863
+ },
864
+ onMouseLeave: (e2) => {
865
+ e2.currentTarget.style.borderColor = "transparent";
866
+ e2.currentTarget.querySelectorAll("[data-delete-btn], [data-edit-btn]").forEach((btn) => btn.style.opacity = "0.6");
867
+ },
868
+ children: [
869
+ item.assetId && /* @__PURE__ */ jsx6(
870
+ "button",
871
+ {
872
+ "data-edit-btn": true,
873
+ title: "Edit details",
874
+ "aria-label": `Edit details for ${item.filename}`,
875
+ tabIndex: -1,
876
+ onClick: (e2) => {
877
+ e2.stopPropagation();
878
+ onStartEdit(item);
879
+ },
880
+ style: {
881
+ position: "absolute",
882
+ top: "4px",
883
+ right: "32px",
884
+ zIndex: 1,
885
+ width: "24px",
886
+ height: "24px",
887
+ padding: 0,
888
+ border: "none",
889
+ borderRadius: "4px",
890
+ backgroundColor: "rgba(0, 0, 0, 0.55)",
891
+ color: "white",
892
+ fontSize: "12px",
893
+ lineHeight: "24px",
894
+ textAlign: "center",
895
+ cursor: "pointer",
896
+ opacity: 0.6,
897
+ transition: "opacity 0.15s",
898
+ display: "flex",
899
+ alignItems: "center",
900
+ justifyContent: "center"
901
+ },
902
+ children: "\u270E"
903
+ }
904
+ ),
905
+ item.assetId && /* @__PURE__ */ jsx6(
906
+ "button",
907
+ {
908
+ "data-delete-btn": true,
909
+ title: "Delete image",
910
+ "aria-label": `Delete ${item.filename}`,
911
+ tabIndex: -1,
912
+ onClick: (e2) => {
913
+ e2.stopPropagation();
914
+ setConfirmDeleteId(item.assetId ?? null);
915
+ },
916
+ style: {
917
+ position: "absolute",
918
+ top: "4px",
919
+ right: "4px",
920
+ zIndex: 1,
921
+ width: "24px",
922
+ height: "24px",
923
+ padding: 0,
924
+ border: "none",
925
+ borderRadius: "4px",
926
+ backgroundColor: "rgba(0, 0, 0, 0.55)",
927
+ color: "white",
928
+ fontSize: "14px",
929
+ lineHeight: "24px",
930
+ textAlign: "center",
931
+ cursor: "pointer",
932
+ opacity: 0.6,
933
+ transition: "opacity 0.15s, background-color 0.15s",
934
+ display: "flex",
935
+ alignItems: "center",
936
+ justifyContent: "center"
937
+ },
938
+ onMouseEnter: (e2) => {
939
+ e2.currentTarget.style.opacity = "1";
940
+ e2.currentTarget.style.backgroundColor = "rgba(220, 38, 38, 0.9)";
941
+ },
942
+ onMouseLeave: (e2) => {
943
+ e2.currentTarget.style.backgroundColor = "rgba(0, 0, 0, 0.55)";
944
+ },
945
+ children: "\xD7"
946
+ }
947
+ ),
948
+ /* @__PURE__ */ jsx6(
949
+ "img",
950
+ {
951
+ src: item.url,
952
+ alt: item.filename,
953
+ loading: "lazy",
954
+ decoding: "async",
955
+ style: { width: "100%", height: "120px", objectFit: "cover", display: "block" }
956
+ }
957
+ ),
958
+ item.assetId && confirmDeleteId === item.assetId && /* @__PURE__ */ jsxs5(
959
+ "div",
960
+ {
961
+ role: "alertdialog",
962
+ "aria-label": `Delete ${item.filename}?`,
963
+ onClick: (e2) => e2.stopPropagation(),
964
+ onKeyDown: (e2) => {
965
+ e2.stopPropagation();
966
+ if (e2.key === "Escape") {
967
+ setConfirmDeleteId(null);
968
+ tileRefs.current[i2]?.focus();
969
+ }
970
+ },
971
+ style: {
972
+ position: "absolute",
973
+ inset: 0,
974
+ zIndex: 2,
975
+ backgroundColor: "rgba(0, 0, 0, 0.72)",
976
+ display: "flex",
977
+ flexDirection: "column",
978
+ alignItems: "center",
979
+ justifyContent: "center",
980
+ gap: "8px",
981
+ padding: "8px"
982
+ },
983
+ children: [
984
+ /* @__PURE__ */ jsx6("div", { style: { color: "white", fontSize: "12px", fontWeight: 600, textAlign: "center" }, children: "Delete this image?" }),
985
+ /* @__PURE__ */ jsxs5("div", { style: { display: "flex", gap: "6px" }, children: [
986
+ /* @__PURE__ */ jsx6(
987
+ "button",
988
+ {
989
+ type: "button",
990
+ autoFocus: true,
991
+ onClick: (e2) => {
992
+ e2.stopPropagation();
993
+ setConfirmDeleteId(null);
994
+ onDeleteItem(item);
995
+ },
996
+ style: {
997
+ padding: "4px 10px",
998
+ backgroundColor: "#dc2626",
999
+ color: "white",
1000
+ border: "none",
1001
+ borderRadius: "4px",
1002
+ fontSize: "12px",
1003
+ fontWeight: 600,
1004
+ cursor: "pointer",
1005
+ fontFamily: "inherit"
1006
+ },
1007
+ children: "Delete"
1008
+ }
1009
+ ),
1010
+ /* @__PURE__ */ jsx6(
1011
+ "button",
1012
+ {
1013
+ type: "button",
1014
+ onClick: (e2) => {
1015
+ e2.stopPropagation();
1016
+ setConfirmDeleteId(null);
1017
+ tileRefs.current[i2]?.focus();
1018
+ },
1019
+ style: {
1020
+ padding: "4px 10px",
1021
+ backgroundColor: "white",
1022
+ color: "#444",
1023
+ border: "none",
1024
+ borderRadius: "4px",
1025
+ fontSize: "12px",
1026
+ fontWeight: 600,
1027
+ cursor: "pointer",
1028
+ fontFamily: "inherit"
1029
+ },
1030
+ children: "Cancel"
1031
+ }
1032
+ )
1033
+ ] })
1034
+ ]
1035
+ }
1036
+ ),
1037
+ /* @__PURE__ */ jsx6(
1038
+ "div",
1039
+ {
1040
+ style: {
1041
+ padding: "6px 8px",
1042
+ fontSize: "11px",
1043
+ color: "#555",
1044
+ overflow: "hidden",
1045
+ textOverflow: "ellipsis",
1046
+ whiteSpace: "nowrap"
1047
+ },
1048
+ title: item.filename,
1049
+ children: item.filename
1050
+ }
1051
+ )
1052
+ ]
1053
+ },
1054
+ item.assetId ?? item.url
1055
+ ))
1056
+ }
1057
+ ),
1058
+ media.length > visibleCount && /* @__PURE__ */ jsx6("div", { ref: sentinelRef, "aria-hidden": "true", style: { height: "1px" } })
1059
+ ] });
1060
+ }
1061
+
1062
+ // src/components/media-library.tsx
1063
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1064
+ var LIST_LIMIT = "500";
1065
+ function siteParams(config) {
1066
+ const params = new URLSearchParams({ siteId: config.siteId });
1067
+ if (config.workstreamId) params.set("workstreamId", config.workstreamId);
1068
+ return params;
1069
+ }
1070
+ function MediaLibrary({ isOpen, onClose, onSelect, onSelectItem }) {
1071
+ const config = useMediaConfig();
1072
+ const schema = orderAltFirst(useMediaSchema());
1073
+ const [media, setMedia] = useState4([]);
1074
+ const [loading, setLoading] = useState4(false);
1075
+ const [uploading, setUploading] = useState4(false);
1076
+ const [searchQuery, setSearchQuery] = useState4("");
1077
+ const [focusIdx, setFocusIdx] = useState4(0);
1078
+ const [pending, setPending] = useState4([]);
1079
+ const [pendingValues, setPendingValues] = useState4([]);
1080
+ const [pendingProgress, setPendingProgress] = useState4([]);
1081
+ const [pendingStatus, setPendingStatus] = useState4([]);
1082
+ const [editing, setEditing] = useState4(null);
1083
+ const [editFields, setEditFields] = useState4([]);
1084
+ const [editValues, setEditValues] = useState4([]);
1085
+ const [panelError, setPanelError] = useState4(null);
1086
+ const replaceInputRef = useRef4(null);
1087
+ const scrollRef = useRef4(null);
1088
+ const getAuthHeaders = useCallback(async () => {
1089
+ const token = await config.getAuthToken();
1090
+ return token ? { Authorization: "Bearer " + token } : {};
1091
+ }, [config]);
1092
+ const fetchMedia = useCallback(
1093
+ async (search) => {
1094
+ setLoading(true);
1095
+ try {
1096
+ const params = siteParams(config);
1097
+ params.set("limit", LIST_LIMIT);
1098
+ if (search) params.set("search", search);
1099
+ const response = await fetch(
1100
+ config.workerUrl + "/media?" + params.toString(),
1101
+ { headers: await getAuthHeaders() }
1102
+ );
1103
+ if (response.ok) {
1104
+ setMedia(parseMediaList(await response.json()));
1105
+ setFocusIdx(0);
1106
+ }
1107
+ } catch (error) {
1108
+ console.error("Failed to fetch media:", error);
1109
+ } finally {
1110
+ setLoading(false);
1111
+ }
1112
+ },
1113
+ [config, getAuthHeaders]
1114
+ );
1115
+ useEffect4(() => {
1116
+ if (isOpen) {
1117
+ fetchMedia();
1118
+ setSearchQuery("");
1119
+ }
1120
+ }, [isOpen, fetchMedia]);
1121
+ const searchInitialized = useRef4(false);
1122
+ useEffect4(() => {
1123
+ if (!isOpen) {
1124
+ searchInitialized.current = false;
1125
+ return;
1126
+ }
1127
+ if (!searchInitialized.current) {
1128
+ searchInitialized.current = true;
1129
+ return;
1130
+ }
1131
+ const timer = setTimeout(() => {
1132
+ fetchMedia(searchQuery || void 0);
1133
+ }, 300);
1134
+ return () => clearTimeout(timer);
1135
+ }, [searchQuery, isOpen, fetchMedia]);
1136
+ const stageFiles = (files) => {
1137
+ if (!files || files.length === 0) return;
1138
+ const staged = Array.from(files).map((file) => ({
1139
+ file,
1140
+ previewUrl: URL.createObjectURL(file)
1141
+ }));
1142
+ setPending(staged);
1143
+ setPendingValues(staged.map(() => schema.map(() => "")));
1144
+ setPendingProgress(staged.map(() => ({})));
1145
+ setPendingStatus(staged.map(() => STAGED_STATUS));
1146
+ setPanelError(null);
1147
+ };
1148
+ const clearPending = useCallback(() => {
1149
+ setPending((prev) => {
1150
+ prev.forEach((p2) => URL.revokeObjectURL(p2.previewUrl));
1151
+ return [];
1152
+ });
1153
+ setPendingValues([]);
1154
+ setPendingProgress([]);
1155
+ setPendingStatus([]);
1156
+ setPanelError(null);
1157
+ }, []);
1158
+ useEffect4(() => {
1159
+ if (!isOpen) {
1160
+ clearPending();
1161
+ setEditing(null);
1162
+ }
1163
+ }, [isOpen, clearPending]);
1164
+ const uploadAll = async () => {
1165
+ setUploading(true);
1166
+ setPanelError(null);
1167
+ const target = {
1168
+ workerUrl: config.workerUrl,
1169
+ siteId: config.siteId,
1170
+ workstreamId: config.workstreamId,
1171
+ getAuthHeaders
1172
+ };
1173
+ const progress = pending.map((_, i2) => pendingProgress[i2] ?? {});
1174
+ const statuses = pending.map((_, i2) => pendingStatus[i2] ?? STAGED_STATUS);
1175
+ for (let i2 = 0; i2 < pending.length; i2++) {
1176
+ const metadata = {};
1177
+ schema.forEach((f2, c2) => {
1178
+ const value = pendingValues[i2]?.[c2]?.trim();
1179
+ if (value) metadata[f2.name] = value;
1180
+ });
1181
+ try {
1182
+ const result = await runUpload(target, pending[i2].file, metadata, void 0, progress[i2], (step) => {
1183
+ statuses[i2] = { step };
1184
+ setPendingStatus([...statuses]);
1185
+ });
1186
+ progress[i2] = result.progress;
1187
+ statuses[i2] = { step: "done" };
1188
+ } catch (error) {
1189
+ const message = error instanceof UploadFlowError ? error.message : "Upload failed";
1190
+ if (error instanceof UploadFlowError) progress[i2] = error.progress;
1191
+ statuses[i2] = { step: "failed", error: message };
1192
+ console.error("Upload failed:", error);
1193
+ }
1194
+ setPendingProgress([...progress]);
1195
+ setPendingStatus([...statuses]);
1196
+ }
1197
+ try {
1198
+ if (statuses.every((s2) => s2.step === "done")) {
1199
+ clearPending();
1200
+ } else {
1201
+ pending.forEach((p2, i2) => {
1202
+ if (statuses[i2].step === "done") URL.revokeObjectURL(p2.previewUrl);
1203
+ });
1204
+ const kept = keepIncompleteRows(pending, pendingValues, progress, statuses);
1205
+ setPending(kept.rows);
1206
+ setPendingValues(kept.values);
1207
+ setPendingProgress(kept.progress);
1208
+ setPendingStatus(kept.statuses);
1209
+ const failures = kept.rows.length;
1210
+ setPanelError(
1211
+ `${failures} of ${pending.length} uploads failed \u2014 successful files are already in the library; only the failed rows below will be retried`
1212
+ );
1213
+ }
1214
+ await fetchMedia(searchQuery || void 0);
1215
+ } finally {
1216
+ setUploading(false);
1217
+ }
1218
+ };
1219
+ const startEdit = (item) => {
1220
+ setEditing(item);
1221
+ setEditFields(schema);
1222
+ setEditValues([schema.map((f2) => item.metadata?.[f2.name] ?? "")]);
1223
+ setPanelError(null);
1224
+ };
1225
+ const saveEdit = async () => {
1226
+ if (!editing?.assetId) return;
1227
+ setUploading(true);
1228
+ setPanelError(null);
1229
+ try {
1230
+ const body = buildPatchBody(editFields, editValues[0] ?? []);
1231
+ const params = siteParams(config);
1232
+ const response = await fetch(
1233
+ config.workerUrl + "/media/" + encodeURIComponent(editing.assetId) + "?" + params.toString(),
1234
+ {
1235
+ method: "PATCH",
1236
+ body: JSON.stringify(body),
1237
+ headers: { ...await getAuthHeaders(), "Content-Type": "application/json" }
1238
+ }
1239
+ );
1240
+ if (response.ok) {
1241
+ setEditing(null);
1242
+ await fetchMedia(searchQuery || void 0);
1243
+ } else {
1244
+ setPanelError("Failed to save metadata");
1245
+ }
1246
+ } catch (error) {
1247
+ console.error("Failed to save metadata:", error);
1248
+ setPanelError("Failed to save metadata");
1249
+ } finally {
1250
+ setUploading(false);
1251
+ }
1252
+ };
1253
+ const replaceImage = async (file) => {
1254
+ if (!file || !editing?.assetId) return;
1255
+ setUploading(true);
1256
+ setPanelError(null);
1257
+ try {
1258
+ const target = {
1259
+ workerUrl: config.workerUrl,
1260
+ siteId: config.siteId,
1261
+ workstreamId: config.workstreamId,
1262
+ getAuthHeaders
1263
+ };
1264
+ const { item } = await runUpload(target, file, void 0, editing.assetId, {}, () => {
1265
+ });
1266
+ setEditing(item);
1267
+ await fetchMedia(searchQuery || void 0);
1268
+ } catch (error) {
1269
+ console.error("Failed to replace image:", error);
1270
+ setPanelError(
1271
+ error instanceof UploadFlowError ? `Failed to replace image: ${error.message}` : "Failed to replace image"
1272
+ );
1273
+ } finally {
1274
+ setUploading(false);
1275
+ if (replaceInputRef.current) replaceInputRef.current.value = "";
1276
+ }
1277
+ };
1278
+ const selectItem = (item) => {
1279
+ if (onSelectItem) {
1280
+ onSelectItem(item);
1281
+ } else {
1282
+ onSelect(item.url);
1283
+ }
1284
+ onClose();
1285
+ };
1286
+ const deleteItem = (item) => {
1287
+ if (!item.assetId) return;
1288
+ (async () => {
1289
+ try {
1290
+ const params = siteParams(config);
1291
+ const response = await fetch(
1292
+ config.workerUrl + "/media/" + encodeURIComponent(item.assetId) + "?" + params.toString(),
1293
+ {
1294
+ method: "DELETE",
1295
+ headers: await getAuthHeaders()
1296
+ }
1297
+ );
1298
+ if (response.ok) {
1299
+ await fetchMedia(searchQuery || void 0);
1300
+ } else {
1301
+ console.error("Failed to delete media:", response.statusText);
1302
+ }
1303
+ } catch (error) {
1304
+ console.error("Failed to delete media:", error);
1305
+ }
1306
+ })();
1307
+ };
1308
+ if (!isOpen) return null;
1309
+ return /* @__PURE__ */ jsx7(
1310
+ "div",
1311
+ {
1312
+ style: {
1313
+ position: "fixed",
1314
+ top: 0,
1315
+ left: 0,
1316
+ right: 0,
1317
+ bottom: 0,
1318
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
1319
+ display: "flex",
1320
+ alignItems: "center",
1321
+ justifyContent: "center",
1322
+ zIndex: 99999,
1323
+ padding: "20px"
1324
+ },
1325
+ onClick: onClose,
1326
+ children: /* @__PURE__ */ jsxs6(
1327
+ "div",
1328
+ {
1329
+ style: {
1330
+ backgroundColor: "white",
1331
+ borderRadius: "12px",
1332
+ boxShadow: "0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04)",
1333
+ maxWidth: "900px",
1334
+ width: "100%",
1335
+ maxHeight: "80vh",
1336
+ display: "flex",
1337
+ flexDirection: "column",
1338
+ overflow: "hidden"
1339
+ },
1340
+ onClick: (e2) => e2.stopPropagation(),
1341
+ children: [
1342
+ /* @__PURE__ */ jsxs6(
1343
+ "div",
1344
+ {
1345
+ style: {
1346
+ padding: "20px 24px",
1347
+ borderBottom: "1px solid #e0e0e0",
1348
+ display: "flex",
1349
+ alignItems: "center",
1350
+ justifyContent: "space-between"
1351
+ },
1352
+ children: [
1353
+ /* @__PURE__ */ jsx7("h2", { style: { margin: 0, fontSize: "20px", fontWeight: 600, color: "#333" }, children: "Media Library" }),
1354
+ /* @__PURE__ */ jsx7(
1355
+ "button",
1356
+ {
1357
+ onClick: onClose,
1358
+ style: {
1359
+ background: "none",
1360
+ border: "none",
1361
+ fontSize: "24px",
1362
+ cursor: "pointer",
1363
+ color: "#666",
1364
+ padding: "0",
1365
+ width: "32px",
1366
+ height: "32px",
1367
+ display: "flex",
1368
+ alignItems: "center",
1369
+ justifyContent: "center",
1370
+ borderRadius: "4px"
1371
+ },
1372
+ children: "\xD7"
1373
+ }
1374
+ )
1375
+ ]
1376
+ }
1377
+ ),
1378
+ /* @__PURE__ */ jsx7("div", { ref: scrollRef, style: { padding: "24px", overflowY: "auto", flex: 1 }, children: pending.length > 0 ? /* @__PURE__ */ jsx7(
1379
+ MediaUploadPanel,
1380
+ {
1381
+ schema,
1382
+ pending,
1383
+ pendingValues,
1384
+ onValuesChange: setPendingValues,
1385
+ pendingStatus,
1386
+ uploading,
1387
+ panelError,
1388
+ onCancel: clearPending,
1389
+ onUpload: uploadAll
1390
+ }
1391
+ ) : editing ? /* @__PURE__ */ jsx7(
1392
+ MediaEditPanel,
1393
+ {
1394
+ editing,
1395
+ editFields,
1396
+ editValues,
1397
+ onValuesChange: setEditValues,
1398
+ uploading,
1399
+ panelError,
1400
+ onCancel: () => setEditing(null),
1401
+ onSave: saveEdit,
1402
+ onReplaceImage: replaceImage,
1403
+ replaceInputRef
1404
+ }
1405
+ ) : /* @__PURE__ */ jsxs6(Fragment2, { children: [
1406
+ /* @__PURE__ */ jsx7(MediaUploadDropzone, { uploading, onFilesSelected: stageFiles }),
1407
+ /* @__PURE__ */ jsx7(
1408
+ MediaGrid,
1409
+ {
1410
+ media,
1411
+ loading,
1412
+ searchQuery,
1413
+ onSearchChange: setSearchQuery,
1414
+ onSelectItem: selectItem,
1415
+ onStartEdit: startEdit,
1416
+ onDeleteItem: deleteItem,
1417
+ scrollRef,
1418
+ focusIdx,
1419
+ onFocusIdxChange: setFocusIdx
1420
+ }
1421
+ )
1422
+ ] }) })
1423
+ ]
1424
+ }
1425
+ )
1426
+ }
1427
+ );
1428
+ }
1429
+
1430
+ // src/crop.ts
1431
+ function getBaseUrl(value) {
1432
+ return value ? value.split("?")[0] : "";
1433
+ }
1434
+ function getParams(value) {
1435
+ return new URLSearchParams(value.includes("?") ? value.split("?")[1] : "");
1436
+ }
1437
+ function getCropMode(value) {
1438
+ if (!value) return "fit";
1439
+ const params = getParams(value);
1440
+ if (params.has("trim.left")) return "custom";
1441
+ return params.get("fit") === "cover" ? "smart" : "fit";
1442
+ }
1443
+ function getTrimRect(value) {
1444
+ if (!value) return null;
1445
+ const params = getParams(value);
1446
+ const read = (key) => {
1447
+ const raw = params.get(key);
1448
+ if (raw === null || raw === "") return null;
1449
+ const n2 = Number(raw);
1450
+ return Number.isFinite(n2) ? n2 : null;
1451
+ };
1452
+ const left = read("trim.left");
1453
+ const top = read("trim.top");
1454
+ const width = read("trim.width");
1455
+ const height = read("trim.height");
1456
+ if (left === null || top === null || width === null || height === null) return null;
1457
+ return { left, top, width, height };
1458
+ }
1459
+ function buildValueWithCrop(baseUrl, crop) {
1460
+ return crop === "smart" ? `${baseUrl}?fit=cover&gravity=auto` : `${baseUrl}?fit=scale-down`;
1461
+ }
1462
+ function buildValueWithTrim(baseUrl, rect) {
1463
+ const left = Math.max(0, Math.round(rect.left));
1464
+ const top = Math.max(0, Math.round(rect.top));
1465
+ const width = Math.max(1, Math.round(rect.width));
1466
+ const height = Math.max(1, Math.round(rect.height));
1467
+ return `${baseUrl}?trim.left=${left}&trim.top=${top}&trim.width=${width}&trim.height=${height}`;
1468
+ }
1469
+
1470
+ // src/components/media-field.tsx
1471
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1472
+ function MediaFieldRender(props) {
1473
+ const { value, onChange, readOnly } = props;
1474
+ const [isLibraryOpen, setIsLibraryOpen] = useState5(false);
1475
+ const baseUrl = getBaseUrl(value);
1476
+ const cropMode = getCropMode(value);
1477
+ const handleCropChange = (crop) => {
1478
+ if (!baseUrl) return;
1479
+ onChange(buildValueWithCrop(baseUrl, crop));
1480
+ };
1481
+ const handleSelect = (url) => {
1482
+ onChange(buildValueWithCrop(url.split("?")[0], cropMode));
1483
+ setIsLibraryOpen(false);
1484
+ };
1485
+ const buttonBase = {
1486
+ padding: "3px 10px",
1487
+ fontSize: "12px",
1488
+ fontWeight: 500,
1489
+ border: "1px solid #d0d0d0",
1490
+ borderRadius: "4px",
1491
+ cursor: readOnly ? "not-allowed" : "pointer",
1492
+ fontFamily: "inherit"
1493
+ };
1494
+ return /* @__PURE__ */ jsxs7("div", { style: { width: "100%" }, children: [
1495
+ value ? /* @__PURE__ */ jsx8("div", { style: { marginBottom: "8px" }, children: /* @__PURE__ */ jsx8(
1496
+ "img",
1497
+ {
1498
+ src: baseUrl,
1499
+ alt: "Preview",
1500
+ style: {
1501
+ width: "100%",
1502
+ maxHeight: "120px",
1503
+ objectFit: "cover",
1504
+ borderRadius: "6px",
1505
+ display: "block",
1506
+ border: "1px solid #e0e0e0"
1507
+ }
1508
+ }
1509
+ ) }) : /* @__PURE__ */ jsx8(
1510
+ "div",
1511
+ {
1512
+ style: {
1513
+ height: "80px",
1514
+ border: "2px dashed #d0d0d0",
1515
+ borderRadius: "6px",
1516
+ display: "flex",
1517
+ alignItems: "center",
1518
+ justifyContent: "center",
1519
+ marginBottom: "8px",
1520
+ color: "#999",
1521
+ fontSize: "13px",
1522
+ cursor: readOnly ? "default" : "pointer"
1523
+ },
1524
+ onClick: () => !readOnly && setIsLibraryOpen(true),
1525
+ children: "Click to select image"
1526
+ }
1527
+ ),
1528
+ value && /* @__PURE__ */ jsxs7("div", { style: { marginBottom: "8px" }, children: [
1529
+ /* @__PURE__ */ jsx8("div", { style: { fontSize: "11px", color: "#666", fontWeight: 500, marginBottom: "4px" }, children: "Crop" }),
1530
+ /* @__PURE__ */ jsxs7("div", { style: { display: "flex", gap: "4px" }, children: [
1531
+ /* @__PURE__ */ jsx8(
1532
+ "button",
1533
+ {
1534
+ type: "button",
1535
+ disabled: readOnly,
1536
+ onClick: () => handleCropChange("fit"),
1537
+ style: {
1538
+ ...buttonBase,
1539
+ backgroundColor: cropMode === "fit" ? "#2563eb" : "white",
1540
+ color: cropMode === "fit" ? "white" : "#444",
1541
+ borderColor: cropMode === "fit" ? "#2563eb" : "#d0d0d0",
1542
+ opacity: readOnly ? 0.5 : 1
1543
+ },
1544
+ children: "Fit in"
1545
+ }
1546
+ ),
1547
+ /* @__PURE__ */ jsx8(
1548
+ "button",
1549
+ {
1550
+ type: "button",
1551
+ disabled: readOnly,
1552
+ onClick: () => handleCropChange("smart"),
1553
+ style: {
1554
+ ...buttonBase,
1555
+ backgroundColor: cropMode === "smart" ? "#2563eb" : "white",
1556
+ color: cropMode === "smart" ? "white" : "#444",
1557
+ borderColor: cropMode === "smart" ? "#2563eb" : "#d0d0d0",
1558
+ opacity: readOnly ? 0.5 : 1
1559
+ },
1560
+ children: "Smart crop"
1561
+ }
1562
+ )
1563
+ ] })
1564
+ ] }),
1565
+ /* @__PURE__ */ jsxs7("div", { style: { display: "flex", gap: "6px" }, children: [
1566
+ /* @__PURE__ */ jsx8(
1567
+ "button",
1568
+ {
1569
+ type: "button",
1570
+ onClick: () => setIsLibraryOpen(true),
1571
+ disabled: readOnly,
1572
+ style: {
1573
+ flex: 1,
1574
+ padding: "6px 12px",
1575
+ backgroundColor: "#2563eb",
1576
+ color: "white",
1577
+ border: "none",
1578
+ borderRadius: "4px",
1579
+ fontSize: "13px",
1580
+ fontWeight: 500,
1581
+ cursor: readOnly ? "not-allowed" : "pointer",
1582
+ opacity: readOnly ? 0.5 : 1,
1583
+ fontFamily: "inherit"
1584
+ },
1585
+ children: "Choose from Library"
1586
+ }
1587
+ ),
1588
+ value && /* @__PURE__ */ jsx8(
1589
+ "button",
1590
+ {
1591
+ type: "button",
1592
+ onClick: () => onChange(""),
1593
+ disabled: readOnly,
1594
+ style: {
1595
+ padding: "6px 12px",
1596
+ backgroundColor: "white",
1597
+ color: "#666",
1598
+ border: "1px solid #d0d0d0",
1599
+ borderRadius: "4px",
1600
+ fontSize: "13px",
1601
+ fontWeight: 500,
1602
+ cursor: readOnly ? "not-allowed" : "pointer",
1603
+ opacity: readOnly ? 0.5 : 1,
1604
+ fontFamily: "inherit"
1605
+ },
1606
+ children: "Clear"
1607
+ }
1608
+ )
1609
+ ] }),
1610
+ /* @__PURE__ */ jsx8(
1611
+ MediaLibrary,
1612
+ {
1613
+ isOpen: isLibraryOpen,
1614
+ onClose: () => setIsLibraryOpen(false),
1615
+ onSelect: handleSelect
1616
+ }
1617
+ )
1618
+ ] });
1619
+ }
1620
+
1621
+ // src/components/media-object-field.tsx
1622
+ import { useState as useState7 } from "react";
1623
+
1624
+ // src/components/crop-dialog.tsx
1625
+ import { useEffect as useEffect5, useRef as useRef5, useState as useState6 } from "react";
1626
+
1627
+ // ../../node_modules/.pnpm/react-image-crop@11.1.2_react@19.2.6/node_modules/react-image-crop/dist/index.js
1628
+ import e, { PureComponent as t, createRef as n } from "react";
1629
+ var r = {
1630
+ x: 0,
1631
+ y: 0,
1632
+ width: 0,
1633
+ height: 0,
1634
+ unit: "px"
1635
+ };
1636
+ var i = (e2, t2, n2) => Math.min(Math.max(e2, t2), n2);
1637
+ var a = (...e2) => e2.filter((e3) => e3 && typeof e3 == "string").join(" ");
1638
+ var o = (e2, t2) => e2 === t2 || e2.width === t2.width && e2.height === t2.height && e2.x === t2.x && e2.y === t2.y && e2.unit === t2.unit;
1639
+ function s(e2, t2, n2, r2) {
1640
+ let i2 = u(e2, n2, r2);
1641
+ return e2.width && (i2.height = i2.width / t2), e2.height && (i2.width = i2.height * t2), i2.y + i2.height > r2 && (i2.height = r2 - i2.y, i2.width = i2.height * t2), i2.x + i2.width > n2 && (i2.width = n2 - i2.x, i2.height = i2.width / t2), e2.unit === "%" ? l(i2, n2, r2) : i2;
1642
+ }
1643
+ function c(e2, t2, n2) {
1644
+ let r2 = u(e2, t2, n2);
1645
+ return r2.x = (t2 - r2.width) / 2, r2.y = (n2 - r2.height) / 2, e2.unit === "%" ? l(r2, t2, n2) : r2;
1646
+ }
1647
+ function l(e2, t2, n2) {
1648
+ return e2.unit === "%" ? {
1649
+ ...r,
1650
+ ...e2,
1651
+ unit: "%"
1652
+ } : {
1653
+ unit: "%",
1654
+ x: e2.x ? e2.x / t2 * 100 : 0,
1655
+ y: e2.y ? e2.y / n2 * 100 : 0,
1656
+ width: e2.width ? e2.width / t2 * 100 : 0,
1657
+ height: e2.height ? e2.height / n2 * 100 : 0
1658
+ };
1659
+ }
1660
+ function u(e2, t2, n2) {
1661
+ return !e2.unit || e2.unit === "px" ? {
1662
+ ...r,
1663
+ ...e2,
1664
+ unit: "px"
1665
+ } : {
1666
+ unit: "px",
1667
+ x: e2.x ? e2.x * t2 / 100 : 0,
1668
+ y: e2.y ? e2.y * n2 / 100 : 0,
1669
+ width: e2.width ? e2.width * t2 / 100 : 0,
1670
+ height: e2.height ? e2.height * n2 / 100 : 0
1671
+ };
1672
+ }
1673
+ function d(e2, t2, n2, r2, i2, a2 = 0, o2 = 0, s2 = r2, c2 = i2) {
1674
+ let l2 = { ...e2 }, u2 = Math.min(a2, r2), d2 = Math.min(o2, i2), f2 = Math.min(s2, r2), p2 = Math.min(c2, i2);
1675
+ t2 && (t2 > 1 ? (u2 = o2 ? o2 * t2 : u2, d2 = u2 / t2, f2 = s2 * t2) : (d2 = a2 ? a2 / t2 : d2, u2 = d2 * t2, p2 = c2 / t2)), l2.y < 0 && (l2.height = Math.max(l2.height + l2.y, d2), l2.y = 0), l2.x < 0 && (l2.width = Math.max(l2.width + l2.x, u2), l2.x = 0);
1676
+ let m2 = r2 - (l2.x + l2.width);
1677
+ m2 < 0 && (l2.x = Math.min(l2.x, r2 - u2), l2.width += m2);
1678
+ let h2 = i2 - (l2.y + l2.height);
1679
+ if (h2 < 0 && (l2.y = Math.min(l2.y, i2 - d2), l2.height += h2), l2.width < u2 && ((n2 === "sw" || n2 == "nw") && (l2.x -= u2 - l2.width), l2.width = u2), l2.height < d2 && ((n2 === "nw" || n2 == "ne") && (l2.y -= d2 - l2.height), l2.height = d2), l2.width > f2 && ((n2 === "sw" || n2 == "nw") && (l2.x -= f2 - l2.width), l2.width = f2), l2.height > p2 && ((n2 === "nw" || n2 == "ne") && (l2.y -= p2 - l2.height), l2.height = p2), t2) {
1680
+ let e3 = l2.width / l2.height;
1681
+ if (e3 < t2) {
1682
+ let e4 = Math.max(l2.width / t2, d2);
1683
+ (n2 === "nw" || n2 == "ne") && (l2.y -= e4 - l2.height), l2.height = e4;
1684
+ } else if (e3 > t2) {
1685
+ let e4 = Math.max(l2.height * t2, u2);
1686
+ (n2 === "sw" || n2 == "nw") && (l2.x -= e4 - l2.width), l2.width = e4;
1687
+ }
1688
+ }
1689
+ return l2;
1690
+ }
1691
+ function f(e2, t2, n2, r2) {
1692
+ let i2 = { ...e2 };
1693
+ return t2 === "ArrowLeft" ? r2 === "nw" ? (i2.x -= n2, i2.y -= n2, i2.width += n2, i2.height += n2) : r2 === "w" ? (i2.x -= n2, i2.width += n2) : r2 === "sw" ? (i2.x -= n2, i2.width += n2, i2.height += n2) : r2 === "ne" ? (i2.y += n2, i2.width -= n2, i2.height -= n2) : r2 === "e" ? i2.width -= n2 : r2 === "se" && (i2.width -= n2, i2.height -= n2) : t2 === "ArrowRight" && (r2 === "nw" ? (i2.x += n2, i2.y += n2, i2.width -= n2, i2.height -= n2) : r2 === "w" ? (i2.x += n2, i2.width -= n2) : r2 === "sw" ? (i2.x += n2, i2.width -= n2, i2.height -= n2) : r2 === "ne" ? (i2.y -= n2, i2.width += n2, i2.height += n2) : r2 === "e" ? i2.width += n2 : r2 === "se" && (i2.width += n2, i2.height += n2)), t2 === "ArrowUp" ? r2 === "nw" ? (i2.x -= n2, i2.y -= n2, i2.width += n2, i2.height += n2) : r2 === "n" ? (i2.y -= n2, i2.height += n2) : r2 === "ne" ? (i2.y -= n2, i2.width += n2, i2.height += n2) : r2 === "sw" ? (i2.x += n2, i2.width -= n2, i2.height -= n2) : r2 === "s" ? i2.height -= n2 : r2 === "se" && (i2.width -= n2, i2.height -= n2) : t2 === "ArrowDown" && (r2 === "nw" ? (i2.x += n2, i2.y += n2, i2.width -= n2, i2.height -= n2) : r2 === "n" ? (i2.y += n2, i2.height -= n2) : r2 === "ne" ? (i2.y += n2, i2.width -= n2, i2.height -= n2) : r2 === "sw" ? (i2.x -= n2, i2.width += n2, i2.height += n2) : r2 === "s" ? i2.height += n2 : r2 === "se" && (i2.width += n2, i2.height += n2)), i2;
1694
+ }
1695
+ var p = {
1696
+ capture: true,
1697
+ passive: false
1698
+ };
1699
+ var m = 0;
1700
+ var _a;
1701
+ var h = (_a = class extends t {
1702
+ constructor() {
1703
+ super(...arguments);
1704
+ __publicField(this, "docMoveBound", false);
1705
+ __publicField(this, "mouseDownOnCrop", false);
1706
+ __publicField(this, "dragStarted", false);
1707
+ __publicField(this, "evData", {
1708
+ startClientX: 0,
1709
+ startClientY: 0,
1710
+ startCropX: 0,
1711
+ startCropY: 0,
1712
+ clientX: 0,
1713
+ clientY: 0,
1714
+ isResize: true
1715
+ });
1716
+ __publicField(this, "componentRef", n());
1717
+ __publicField(this, "mediaRef", n());
1718
+ __publicField(this, "resizeObserver");
1719
+ __publicField(this, "initChangeCalled", false);
1720
+ __publicField(this, "instanceId", `rc-${m++}`);
1721
+ __publicField(this, "state", {
1722
+ cropIsActive: false,
1723
+ newCropIsBeingDrawn: false
1724
+ });
1725
+ __publicField(this, "onCropPointerDown", (e2) => {
1726
+ let { crop: t2, disabled: n2 } = this.props, r2 = this.getBox();
1727
+ if (!t2) return;
1728
+ let i2 = u(t2, r2.width, r2.height);
1729
+ if (n2) return;
1730
+ e2.cancelable && e2.preventDefault(), this.bindDocMove(), this.componentRef.current.focus({ preventScroll: true });
1731
+ let a2 = e2.target.dataset.ord, o2 = !!a2, s2 = e2.clientX, c2 = e2.clientY, l2 = i2.x, d2 = i2.y;
1732
+ if (a2) {
1733
+ let t3 = e2.clientX - r2.x, n3 = e2.clientY - r2.y, o3 = 0, u2 = 0;
1734
+ a2 === "ne" || a2 == "e" ? (o3 = t3 - (i2.x + i2.width), u2 = n3 - i2.y, l2 = i2.x, d2 = i2.y + i2.height) : a2 === "se" || a2 === "s" ? (o3 = t3 - (i2.x + i2.width), u2 = n3 - (i2.y + i2.height), l2 = i2.x, d2 = i2.y) : a2 === "sw" || a2 == "w" ? (o3 = t3 - i2.x, u2 = n3 - (i2.y + i2.height), l2 = i2.x + i2.width, d2 = i2.y) : (a2 === "nw" || a2 == "n") && (o3 = t3 - i2.x, u2 = n3 - i2.y, l2 = i2.x + i2.width, d2 = i2.y + i2.height), s2 = l2 + r2.x + o3, c2 = d2 + r2.y + u2;
1735
+ }
1736
+ this.evData = {
1737
+ startClientX: s2,
1738
+ startClientY: c2,
1739
+ startCropX: l2,
1740
+ startCropY: d2,
1741
+ clientX: e2.clientX,
1742
+ clientY: e2.clientY,
1743
+ isResize: o2,
1744
+ ord: a2
1745
+ }, this.mouseDownOnCrop = true, this.setState({ cropIsActive: true });
1746
+ });
1747
+ __publicField(this, "onComponentPointerDown", (e2) => {
1748
+ let { crop: t2, disabled: n2, locked: r2, keepSelection: i2, onChange: a2 } = this.props, o2 = this.getBox();
1749
+ if (n2 || r2 || i2 && t2) return;
1750
+ e2.cancelable && e2.preventDefault(), this.bindDocMove(), this.componentRef.current.focus({ preventScroll: true });
1751
+ let s2 = e2.clientX - o2.x, c2 = e2.clientY - o2.y, d2 = {
1752
+ unit: "px",
1753
+ x: s2,
1754
+ y: c2,
1755
+ width: 0,
1756
+ height: 0
1757
+ };
1758
+ this.evData = {
1759
+ startClientX: e2.clientX,
1760
+ startClientY: e2.clientY,
1761
+ startCropX: s2,
1762
+ startCropY: c2,
1763
+ clientX: e2.clientX,
1764
+ clientY: e2.clientY,
1765
+ isResize: true
1766
+ }, this.mouseDownOnCrop = true, a2(u(d2, o2.width, o2.height), l(d2, o2.width, o2.height)), this.setState({
1767
+ cropIsActive: true,
1768
+ newCropIsBeingDrawn: true
1769
+ });
1770
+ });
1771
+ __publicField(this, "onDocPointerMove", (e2) => {
1772
+ let { crop: t2, disabled: n2, onChange: r2, onDragStart: i2 } = this.props, a2 = this.getBox();
1773
+ if (n2 || !t2 || !this.mouseDownOnCrop) return;
1774
+ e2.cancelable && e2.preventDefault(), this.dragStarted || (this.dragStarted = true, i2 && i2(e2));
1775
+ let { evData: s2 } = this;
1776
+ s2.clientX = e2.clientX, s2.clientY = e2.clientY;
1777
+ let c2;
1778
+ c2 = s2.isResize ? this.resizeCrop() : this.dragCrop(), o(t2, c2) || r2(u(c2, a2.width, a2.height), l(c2, a2.width, a2.height));
1779
+ });
1780
+ __publicField(this, "onComponentKeyDown", (e2) => {
1781
+ let { crop: t2, disabled: n2, onChange: r2, onComplete: a2 } = this.props;
1782
+ if (n2) return;
1783
+ let o2 = e2.key, c2 = false;
1784
+ if (!t2) return;
1785
+ let d2 = this.getBox(), f2 = this.makePixelCrop(d2), p2 = (navigator.platform.match("Mac") ? e2.metaKey : e2.ctrlKey) ? _a.nudgeStepLarge : e2.shiftKey ? _a.nudgeStepMedium : _a.nudgeStep;
1786
+ if (o2 === "ArrowLeft" ? (f2.x -= p2, c2 = true) : o2 === "ArrowRight" ? (f2.x += p2, c2 = true) : o2 === "ArrowUp" ? (f2.y -= p2, c2 = true) : o2 === "ArrowDown" && (f2.y += p2, c2 = true), c2) {
1787
+ e2.cancelable && e2.preventDefault(), f2.x = i(f2.x, 0, d2.width - f2.width), f2.y = i(f2.y, 0, d2.height - f2.height);
1788
+ let t3 = u(f2, d2.width, d2.height), n3 = l(f2, d2.width, d2.height);
1789
+ r2(t3, n3), a2 && a2(t3, n3);
1790
+ }
1791
+ });
1792
+ __publicField(this, "onHandlerKeyDown", (e2, t2) => {
1793
+ let { aspect: n2 = 0, crop: r2, disabled: i2, minWidth: a2 = 0, minHeight: c2 = 0, maxWidth: p2, maxHeight: m2, onChange: h2, onComplete: g2 } = this.props, _ = this.getBox();
1794
+ if (i2 || !r2) return;
1795
+ if (e2.key === "ArrowUp" || e2.key === "ArrowDown" || e2.key === "ArrowLeft" || e2.key === "ArrowRight") e2.stopPropagation(), e2.preventDefault();
1796
+ else return;
1797
+ let v = (navigator.platform.match("Mac") ? e2.metaKey : e2.ctrlKey) ? _a.nudgeStepLarge : e2.shiftKey ? _a.nudgeStepMedium : _a.nudgeStep, y = d(f(u(r2, _.width, _.height), e2.key, v, t2), n2, t2, _.width, _.height, a2, c2, p2, m2);
1798
+ if (!o(r2, y)) {
1799
+ let e3 = l(y, _.width, _.height);
1800
+ h2(y, e3), g2 && g2(y, e3);
1801
+ }
1802
+ });
1803
+ __publicField(this, "onDocPointerDone", (e2) => {
1804
+ let { crop: t2, disabled: n2, onComplete: r2, onDragEnd: i2 } = this.props, a2 = this.getBox();
1805
+ this.unbindDocMove(), !(n2 || !t2) && this.mouseDownOnCrop && (this.mouseDownOnCrop = false, this.dragStarted = false, i2 && i2(e2), r2 && r2(u(t2, a2.width, a2.height), l(t2, a2.width, a2.height)), this.setState({
1806
+ cropIsActive: false,
1807
+ newCropIsBeingDrawn: false
1808
+ }));
1809
+ });
1810
+ __publicField(this, "onDragFocus", () => {
1811
+ this.componentRef.current?.scrollTo(0, 0);
1812
+ });
1813
+ }
1814
+ get document() {
1815
+ return document;
1816
+ }
1817
+ getBox() {
1818
+ let e2 = this.mediaRef.current;
1819
+ if (!e2) return {
1820
+ x: 0,
1821
+ y: 0,
1822
+ width: 0,
1823
+ height: 0
1824
+ };
1825
+ let { x: t2, y: n2, width: r2, height: i2 } = e2.getBoundingClientRect();
1826
+ return {
1827
+ x: t2,
1828
+ y: n2,
1829
+ width: r2,
1830
+ height: i2
1831
+ };
1832
+ }
1833
+ componentDidUpdate(e2) {
1834
+ let { crop: t2, onComplete: n2 } = this.props;
1835
+ if (n2 && !e2.crop && t2) {
1836
+ let { width: e3, height: r2 } = this.getBox();
1837
+ e3 && r2 && n2(u(t2, e3, r2), l(t2, e3, r2));
1838
+ }
1839
+ }
1840
+ componentWillUnmount() {
1841
+ this.resizeObserver && this.resizeObserver.disconnect(), this.unbindDocMove();
1842
+ }
1843
+ bindDocMove() {
1844
+ this.docMoveBound || (this.docMoveBound = (this.document.addEventListener("pointermove", this.onDocPointerMove, p), this.document.addEventListener("pointerup", this.onDocPointerDone, p), this.document.addEventListener("pointercancel", this.onDocPointerDone, p), true));
1845
+ }
1846
+ unbindDocMove() {
1847
+ this.docMoveBound && (this.docMoveBound = (this.document.removeEventListener("pointermove", this.onDocPointerMove, p), this.document.removeEventListener("pointerup", this.onDocPointerDone, p), this.document.removeEventListener("pointercancel", this.onDocPointerDone, p), false));
1848
+ }
1849
+ getCropStyle() {
1850
+ let { crop: e2 } = this.props;
1851
+ if (e2) return {
1852
+ top: `${e2.y}${e2.unit}`,
1853
+ left: `${e2.x}${e2.unit}`,
1854
+ width: `${e2.width}${e2.unit}`,
1855
+ height: `${e2.height}${e2.unit}`
1856
+ };
1857
+ }
1858
+ dragCrop() {
1859
+ let { evData: e2 } = this, t2 = this.getBox(), n2 = this.makePixelCrop(t2), r2 = e2.clientX - e2.startClientX, a2 = e2.clientY - e2.startClientY;
1860
+ return n2.x = i(e2.startCropX + r2, 0, t2.width - n2.width), n2.y = i(e2.startCropY + a2, 0, t2.height - n2.height), n2;
1861
+ }
1862
+ getPointRegion(e2, t2, n2, r2) {
1863
+ let { evData: i2 } = this, a2 = i2.clientX - e2.x, o2 = i2.clientY - e2.y, s2;
1864
+ s2 = r2 && t2 ? t2 === "nw" || t2 === "n" || t2 === "ne" : o2 < i2.startCropY;
1865
+ let c2;
1866
+ return c2 = n2 && t2 ? t2 === "nw" || t2 === "w" || t2 === "sw" : a2 < i2.startCropX, c2 ? s2 ? "nw" : "sw" : s2 ? "ne" : "se";
1867
+ }
1868
+ resolveMinDimensions(e2, t2, n2 = 0, r2 = 0) {
1869
+ let i2 = Math.min(n2, e2.width), a2 = Math.min(r2, e2.height);
1870
+ return !t2 || !i2 && !a2 ? [i2, a2] : t2 > 1 ? i2 ? [i2, i2 / t2] : [a2 * t2, a2] : a2 ? [a2 * t2, a2] : [i2, i2 / t2];
1871
+ }
1872
+ resizeCrop() {
1873
+ let { evData: e2 } = this, { aspect: t2 = 0, maxWidth: n2, maxHeight: r2 } = this.props, a2 = this.getBox(), [o2, c2] = this.resolveMinDimensions(a2, t2, this.props.minWidth, this.props.minHeight), l2 = this.makePixelCrop(a2), u2 = this.getPointRegion(a2, e2.ord, o2, c2), f2 = e2.ord || u2, p2 = e2.clientX - e2.startClientX, m2 = e2.clientY - e2.startClientY;
1874
+ (o2 && f2 === "nw" || f2 === "w" || f2 === "sw") && (p2 = Math.min(p2, -o2)), (c2 && f2 === "nw" || f2 === "n" || f2 === "ne") && (m2 = Math.min(m2, -c2));
1875
+ let h2 = {
1876
+ unit: "px",
1877
+ x: 0,
1878
+ y: 0,
1879
+ width: 0,
1880
+ height: 0
1881
+ };
1882
+ u2 === "ne" ? (h2.x = e2.startCropX, h2.width = p2, t2 ? (h2.height = h2.width / t2, h2.y = e2.startCropY - h2.height) : (h2.height = Math.abs(m2), h2.y = e2.startCropY - h2.height)) : u2 === "se" ? (h2.x = e2.startCropX, h2.y = e2.startCropY, h2.width = p2, t2 ? h2.height = h2.width / t2 : h2.height = m2) : u2 === "sw" ? (h2.x = e2.startCropX + p2, h2.y = e2.startCropY, h2.width = Math.abs(p2), t2 ? h2.height = h2.width / t2 : h2.height = m2) : u2 === "nw" && (h2.x = e2.startCropX + p2, h2.width = Math.abs(p2), t2 ? (h2.height = h2.width / t2, h2.y = e2.startCropY - h2.height) : (h2.height = Math.abs(m2), h2.y = e2.startCropY + m2));
1883
+ let g2 = d(h2, t2, u2, a2.width, a2.height, o2, c2, n2, r2);
1884
+ return t2 || _a.xyOrds.indexOf(f2) > -1 ? l2 = g2 : _a.xOrds.indexOf(f2) > -1 ? (l2.x = g2.x, l2.width = g2.width) : _a.yOrds.indexOf(f2) > -1 && (l2.y = g2.y, l2.height = g2.height), l2.x = i(l2.x, 0, a2.width - l2.width), l2.y = i(l2.y, 0, a2.height - l2.height), l2;
1885
+ }
1886
+ renderCropSelection() {
1887
+ let { ariaLabels: t2 = _a.defaultProps.ariaLabels, disabled: n2, locked: r2, renderSelectionAddon: i2, ruleOfThirds: a2, crop: o2 } = this.props, c2 = this.getCropStyle();
1888
+ if (o2) return /* @__PURE__ */ e.createElement("div", {
1889
+ style: c2,
1890
+ className: "ReactCrop__crop-selection",
1891
+ onPointerDown: this.onCropPointerDown,
1892
+ "aria-label": t2.cropArea,
1893
+ tabIndex: 0,
1894
+ onKeyDown: this.onComponentKeyDown,
1895
+ role: "group"
1896
+ }, !n2 && !r2 && /* @__PURE__ */ e.createElement("div", {
1897
+ className: "ReactCrop__drag-elements",
1898
+ onFocus: this.onDragFocus
1899
+ }, /* @__PURE__ */ e.createElement("div", {
1900
+ className: "ReactCrop__drag-bar ord-n",
1901
+ "data-ord": "n"
1902
+ }), /* @__PURE__ */ e.createElement("div", {
1903
+ className: "ReactCrop__drag-bar ord-e",
1904
+ "data-ord": "e"
1905
+ }), /* @__PURE__ */ e.createElement("div", {
1906
+ className: "ReactCrop__drag-bar ord-s",
1907
+ "data-ord": "s"
1908
+ }), /* @__PURE__ */ e.createElement("div", {
1909
+ className: "ReactCrop__drag-bar ord-w",
1910
+ "data-ord": "w"
1911
+ }), /* @__PURE__ */ e.createElement("div", {
1912
+ className: "ReactCrop__drag-handle ord-nw",
1913
+ "data-ord": "nw",
1914
+ tabIndex: 0,
1915
+ "aria-label": t2.nwDragHandle,
1916
+ onKeyDown: (e2) => this.onHandlerKeyDown(e2, "nw"),
1917
+ role: "button"
1918
+ }), /* @__PURE__ */ e.createElement("div", {
1919
+ className: "ReactCrop__drag-handle ord-n",
1920
+ "data-ord": "n",
1921
+ tabIndex: 0,
1922
+ "aria-label": t2.nDragHandle,
1923
+ onKeyDown: (e2) => this.onHandlerKeyDown(e2, "n"),
1924
+ role: "button"
1925
+ }), /* @__PURE__ */ e.createElement("div", {
1926
+ className: "ReactCrop__drag-handle ord-ne",
1927
+ "data-ord": "ne",
1928
+ tabIndex: 0,
1929
+ "aria-label": t2.neDragHandle,
1930
+ onKeyDown: (e2) => this.onHandlerKeyDown(e2, "ne"),
1931
+ role: "button"
1932
+ }), /* @__PURE__ */ e.createElement("div", {
1933
+ className: "ReactCrop__drag-handle ord-e",
1934
+ "data-ord": "e",
1935
+ tabIndex: 0,
1936
+ "aria-label": t2.eDragHandle,
1937
+ onKeyDown: (e2) => this.onHandlerKeyDown(e2, "e"),
1938
+ role: "button"
1939
+ }), /* @__PURE__ */ e.createElement("div", {
1940
+ className: "ReactCrop__drag-handle ord-se",
1941
+ "data-ord": "se",
1942
+ tabIndex: 0,
1943
+ "aria-label": t2.seDragHandle,
1944
+ onKeyDown: (e2) => this.onHandlerKeyDown(e2, "se"),
1945
+ role: "button"
1946
+ }), /* @__PURE__ */ e.createElement("div", {
1947
+ className: "ReactCrop__drag-handle ord-s",
1948
+ "data-ord": "s",
1949
+ tabIndex: 0,
1950
+ "aria-label": t2.sDragHandle,
1951
+ onKeyDown: (e2) => this.onHandlerKeyDown(e2, "s"),
1952
+ role: "button"
1953
+ }), /* @__PURE__ */ e.createElement("div", {
1954
+ className: "ReactCrop__drag-handle ord-sw",
1955
+ "data-ord": "sw",
1956
+ tabIndex: 0,
1957
+ "aria-label": t2.swDragHandle,
1958
+ onKeyDown: (e2) => this.onHandlerKeyDown(e2, "sw"),
1959
+ role: "button"
1960
+ }), /* @__PURE__ */ e.createElement("div", {
1961
+ className: "ReactCrop__drag-handle ord-w",
1962
+ "data-ord": "w",
1963
+ tabIndex: 0,
1964
+ "aria-label": t2.wDragHandle,
1965
+ onKeyDown: (e2) => this.onHandlerKeyDown(e2, "w"),
1966
+ role: "button"
1967
+ })), i2 && /* @__PURE__ */ e.createElement("div", {
1968
+ className: "ReactCrop__selection-addon",
1969
+ onPointerDown: (e2) => e2.stopPropagation()
1970
+ }, i2(this.state)), a2 && /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement("div", { className: "ReactCrop__rule-of-thirds-hz" }), /* @__PURE__ */ e.createElement("div", { className: "ReactCrop__rule-of-thirds-vt" })));
1971
+ }
1972
+ makePixelCrop(e2) {
1973
+ return u({
1974
+ ...r,
1975
+ ...this.props.crop || {}
1976
+ }, e2.width, e2.height);
1977
+ }
1978
+ render() {
1979
+ let { aspect: t2, children: n2, circularCrop: r2, className: i2, crop: o2, disabled: s2, locked: c2, style: l2, ruleOfThirds: u2 } = this.props, { cropIsActive: d2, newCropIsBeingDrawn: f2 } = this.state, p2 = o2 ? this.renderCropSelection() : null, m2 = a("ReactCrop", i2, d2 && "ReactCrop--active", s2 && "ReactCrop--disabled", c2 && "ReactCrop--locked", f2 && "ReactCrop--new-crop", o2 && t2 && "ReactCrop--fixed-aspect", o2 && r2 && "ReactCrop--circular-crop", o2 && u2 && "ReactCrop--rule-of-thirds", !this.dragStarted && o2 && !o2.width && !o2.height && "ReactCrop--invisible-crop", r2 && "ReactCrop--no-animate");
1980
+ return /* @__PURE__ */ e.createElement("div", {
1981
+ ref: this.componentRef,
1982
+ className: m2,
1983
+ style: l2
1984
+ }, /* @__PURE__ */ e.createElement("div", {
1985
+ ref: this.mediaRef,
1986
+ className: "ReactCrop__child-wrapper",
1987
+ onPointerDown: this.onComponentPointerDown
1988
+ }, n2), o2 ? /* @__PURE__ */ e.createElement("svg", {
1989
+ className: "ReactCrop__crop-mask",
1990
+ width: "100%",
1991
+ height: "100%"
1992
+ }, /* @__PURE__ */ e.createElement("defs", null, /* @__PURE__ */ e.createElement("mask", { id: `hole-${this.instanceId}` }, /* @__PURE__ */ e.createElement("rect", {
1993
+ width: "100%",
1994
+ height: "100%",
1995
+ fill: "white"
1996
+ }), r2 ? /* @__PURE__ */ e.createElement("ellipse", {
1997
+ cx: `${o2.x + o2.width / 2}${o2.unit}`,
1998
+ cy: `${o2.y + o2.height / 2}${o2.unit}`,
1999
+ rx: `${o2.width / 2}${o2.unit}`,
2000
+ ry: `${o2.height / 2}${o2.unit}`,
2001
+ fill: "black"
2002
+ }) : /* @__PURE__ */ e.createElement("rect", {
2003
+ x: `${o2.x}${o2.unit}`,
2004
+ y: `${o2.y}${o2.unit}`,
2005
+ width: `${o2.width}${o2.unit}`,
2006
+ height: `${o2.height}${o2.unit}`,
2007
+ fill: "black"
2008
+ }))), /* @__PURE__ */ e.createElement("rect", {
2009
+ fill: "black",
2010
+ fillOpacity: 0.5,
2011
+ width: "100%",
2012
+ height: "100%",
2013
+ mask: `url(#hole-${this.instanceId})`
2014
+ })) : void 0, p2);
2015
+ }
2016
+ }, __publicField(_a, "xOrds", ["e", "w"]), __publicField(_a, "yOrds", ["n", "s"]), __publicField(_a, "xyOrds", [
2017
+ "nw",
2018
+ "ne",
2019
+ "se",
2020
+ "sw"
2021
+ ]), __publicField(_a, "nudgeStep", 1), __publicField(_a, "nudgeStepMedium", 10), __publicField(_a, "nudgeStepLarge", 100), __publicField(_a, "defaultProps", { ariaLabels: {
2022
+ cropArea: "Use the arrow keys to move the crop selection area",
2023
+ nwDragHandle: "Use the arrow keys to move the north west drag handle to change the crop selection area",
2024
+ nDragHandle: "Use the up and down arrow keys to move the north drag handle to change the crop selection area",
2025
+ neDragHandle: "Use the arrow keys to move the north east drag handle to change the crop selection area",
2026
+ eDragHandle: "Use the up and down arrow keys to move the east drag handle to change the crop selection area",
2027
+ seDragHandle: "Use the arrow keys to move the south east drag handle to change the crop selection area",
2028
+ sDragHandle: "Use the up and down arrow keys to move the south drag handle to change the crop selection area",
2029
+ swDragHandle: "Use the arrow keys to move the south west drag handle to change the crop selection area",
2030
+ wDragHandle: "Use the up and down arrow keys to move the west drag handle to change the crop selection area"
2031
+ } }), _a);
2032
+ var g = Math.PI / 180;
2033
+
2034
+ // #style-inject:#style-inject
2035
+ function styleInject(css, { insertAt } = {}) {
2036
+ if (!css || typeof document === "undefined") return;
2037
+ const head = document.head || document.getElementsByTagName("head")[0];
2038
+ const style = document.createElement("style");
2039
+ style.type = "text/css";
2040
+ if (insertAt === "top") {
2041
+ if (head.firstChild) {
2042
+ head.insertBefore(style, head.firstChild);
2043
+ } else {
2044
+ head.appendChild(style);
2045
+ }
2046
+ } else {
2047
+ head.appendChild(style);
2048
+ }
2049
+ if (style.styleSheet) {
2050
+ style.styleSheet.cssText = css;
2051
+ } else {
2052
+ style.appendChild(document.createTextNode(css));
2053
+ }
2054
+ }
2055
+
2056
+ // ../../node_modules/.pnpm/react-image-crop@11.1.2_react@19.2.6/node_modules/react-image-crop/dist/ReactCrop.css
2057
+ styleInject('@keyframes marching-ants {\n 0% {\n background-position:\n 0 0,\n 0 100%,\n 0 0,\n 100% 0;\n }\n to {\n background-position:\n 20px 0,\n -20px 100%,\n 0 -20px,\n 100% 20px;\n }\n}\n:root {\n --rc-drag-handle-size:12px;\n --rc-drag-handle-mobile-size:24px;\n --rc-drag-handle-bg-colour:#0003;\n --rc-drag-bar-size:6px;\n --rc-border-color:#ffffffb3;\n --rc-focus-color:#08f;\n}\n.ReactCrop {\n cursor: crosshair;\n max-width: 100%;\n display: inline-block;\n position: relative;\n}\n.ReactCrop *,\n.ReactCrop :before,\n.ReactCrop :after {\n box-sizing: border-box;\n}\n.ReactCrop--disabled,\n.ReactCrop--locked {\n cursor: inherit;\n}\n.ReactCrop__child-wrapper {\n max-height: inherit;\n overflow: hidden;\n}\n.ReactCrop__child-wrapper > img,\n.ReactCrop__child-wrapper > video {\n max-width: 100%;\n max-height: inherit;\n display: block;\n}\n.ReactCrop:not(.ReactCrop--disabled) .ReactCrop__child-wrapper > img,\n.ReactCrop:not(.ReactCrop--disabled) .ReactCrop__child-wrapper > video,\n.ReactCrop:not(.ReactCrop--disabled) .ReactCrop__crop-selection {\n touch-action: none;\n}\n.ReactCrop__crop-mask {\n pointer-events: none;\n width: calc(100% + .5px);\n height: calc(100% + .5px);\n position: absolute;\n inset: 0;\n}\n.ReactCrop__crop-selection {\n cursor: move;\n position: absolute;\n top: 0;\n left: 0;\n transform: translate(0, 0);\n}\n.ReactCrop--disabled .ReactCrop__crop-selection {\n cursor: inherit;\n}\n.ReactCrop--circular-crop .ReactCrop__crop-selection {\n border-radius: 50%;\n}\n.ReactCrop--circular-crop .ReactCrop__crop-selection:after {\n pointer-events: none;\n content: "";\n border: 1px solid var(--rc-border-color);\n opacity: .3;\n position: absolute;\n inset: -1px;\n}\n.ReactCrop--no-animate .ReactCrop__crop-selection {\n outline: 1px dashed #fff;\n}\n.ReactCrop__crop-selection:not(.ReactCrop--no-animate .ReactCrop__crop-selection) {\n color: #fff;\n background-image:\n linear-gradient(\n 90deg,\n #fff 50%,\n #444 50%),\n linear-gradient(\n 90deg,\n #fff 50%,\n #444 50%),\n linear-gradient(#fff 50%, #444 50%),\n linear-gradient(#fff 50%, #444 50%);\n background-position:\n 0 0,\n 0 100%,\n 0 0,\n 100% 0;\n background-repeat:\n repeat-x,\n repeat-x,\n repeat-y,\n repeat-y;\n background-size:\n 10px 1px,\n 10px 1px,\n 1px 10px,\n 1px 10px;\n animation: 1s linear infinite marching-ants;\n}\n.ReactCrop__crop-selection:focus {\n outline: 2px solid var(--rc-focus-color);\n outline-offset: -1px;\n}\n.ReactCrop--invisible-crop .ReactCrop__crop-mask,\n.ReactCrop--invisible-crop .ReactCrop__crop-selection {\n display: none;\n}\n.ReactCrop__rule-of-thirds-vt:before,\n.ReactCrop__rule-of-thirds-vt:after,\n.ReactCrop__rule-of-thirds-hz:before,\n.ReactCrop__rule-of-thirds-hz:after {\n content: "";\n background-color: #fff6;\n display: block;\n position: absolute;\n}\n.ReactCrop__rule-of-thirds-vt:before,\n.ReactCrop__rule-of-thirds-vt:after {\n width: 1px;\n height: 100%;\n}\n.ReactCrop__rule-of-thirds-vt:before {\n left: 33.3333%;\n}\n.ReactCrop__rule-of-thirds-vt:after {\n left: 66.6667%;\n}\n.ReactCrop__rule-of-thirds-hz:before,\n.ReactCrop__rule-of-thirds-hz:after {\n width: 100%;\n height: 1px;\n}\n.ReactCrop__rule-of-thirds-hz:before {\n top: 33.3333%;\n}\n.ReactCrop__rule-of-thirds-hz:after {\n top: 66.6667%;\n}\n.ReactCrop__drag-handle {\n width: var(--rc-drag-handle-size);\n height: var(--rc-drag-handle-size);\n background-color: var(--rc-drag-handle-bg-colour);\n border: 1px solid var(--rc-border-color);\n position: absolute;\n}\n.ReactCrop__drag-handle:focus {\n background: var(--rc-focus-color);\n}\n.ReactCrop .ord-nw {\n cursor: nw-resize;\n top: 0;\n left: 0;\n transform: translate(-50%, -50%);\n}\n.ReactCrop .ord-n {\n cursor: n-resize;\n top: 0;\n left: 50%;\n transform: translate(-50%, -50%);\n}\n.ReactCrop .ord-ne {\n cursor: ne-resize;\n top: 0;\n right: 0;\n transform: translate(50%, -50%);\n}\n.ReactCrop .ord-e {\n cursor: e-resize;\n top: 50%;\n right: 0;\n transform: translate(50%, -50%);\n}\n.ReactCrop .ord-se {\n cursor: se-resize;\n bottom: 0;\n right: 0;\n transform: translate(50%, 50%);\n}\n.ReactCrop .ord-s {\n cursor: s-resize;\n bottom: 0;\n left: 50%;\n transform: translate(-50%, 50%);\n}\n.ReactCrop .ord-sw {\n cursor: sw-resize;\n bottom: 0;\n left: 0;\n transform: translate(-50%, 50%);\n}\n.ReactCrop .ord-w {\n cursor: w-resize;\n top: 50%;\n left: 0;\n transform: translate(-50%, -50%);\n}\n.ReactCrop__disabled .ReactCrop__drag-handle {\n cursor: inherit;\n}\n.ReactCrop__drag-bar {\n position: absolute;\n}\n.ReactCrop__drag-bar.ord-n {\n width: 100%;\n height: var(--rc-drag-bar-size);\n top: 0;\n left: 0;\n transform: translateY(-50%);\n}\n.ReactCrop__drag-bar.ord-e {\n width: var(--rc-drag-bar-size);\n height: 100%;\n top: 0;\n right: 0;\n transform: translate(50%);\n}\n.ReactCrop__drag-bar.ord-s {\n width: 100%;\n height: var(--rc-drag-bar-size);\n bottom: 0;\n left: 0;\n transform: translateY(50%);\n}\n.ReactCrop__drag-bar.ord-w {\n width: var(--rc-drag-bar-size);\n height: 100%;\n top: 0;\n left: 0;\n transform: translate(-50%);\n}\n.ReactCrop--new-crop .ReactCrop__drag-bar,\n.ReactCrop--new-crop .ReactCrop__drag-handle,\n.ReactCrop--fixed-aspect .ReactCrop__drag-bar,\n.ReactCrop--fixed-aspect .ReactCrop__drag-handle.ord-n,\n.ReactCrop--fixed-aspect .ReactCrop__drag-handle.ord-e,\n.ReactCrop--fixed-aspect .ReactCrop__drag-handle.ord-s,\n.ReactCrop--fixed-aspect .ReactCrop__drag-handle.ord-w {\n display: none;\n}\n@media (pointer: coarse) {\n .ReactCrop .ord-n,\n .ReactCrop .ord-e,\n .ReactCrop .ord-s,\n .ReactCrop .ord-w {\n display: none;\n }\n .ReactCrop__drag-handle {\n width: var(--rc-drag-handle-mobile-size);\n height: var(--rc-drag-handle-mobile-size);\n }\n}\n');
2058
+
2059
+ // src/components/crop-dialog.tsx
2060
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2061
+ var ASPECT_PRESETS = [
2062
+ { label: "Free", value: void 0 },
2063
+ { label: "1:1", value: 1 },
2064
+ { label: "4:3", value: 4 / 3 },
2065
+ { label: "3:2", value: 3 / 2 },
2066
+ { label: "16:9", value: 16 / 9 }
2067
+ ];
2068
+ function centeredCrop(naturalWidth, naturalHeight, aspect) {
2069
+ if (!aspect) {
2070
+ return { unit: "%", x: 10, y: 10, width: 80, height: 80 };
2071
+ }
2072
+ return c(
2073
+ s({ unit: "%", width: 80 }, aspect, naturalWidth, naturalHeight),
2074
+ naturalWidth,
2075
+ naturalHeight
2076
+ );
2077
+ }
2078
+ function trimToPercentCrop(trim, naturalWidth, naturalHeight) {
2079
+ return {
2080
+ unit: "%",
2081
+ x: trim.left / naturalWidth * 100,
2082
+ y: trim.top / naturalHeight * 100,
2083
+ width: trim.width / naturalWidth * 100,
2084
+ height: trim.height / naturalHeight * 100
2085
+ };
2086
+ }
2087
+ function CropDialog(props) {
2088
+ const { isOpen, imageUrl, initialTrim, onApply, onClose } = props;
2089
+ const [crop, setCrop] = useState6(void 0);
2090
+ const [aspect, setAspect] = useState6(void 0);
2091
+ const [imageLoaded, setImageLoaded] = useState6(false);
2092
+ const naturalSize = useRef5(null);
2093
+ useEffect5(() => {
2094
+ if (isOpen) {
2095
+ setCrop(void 0);
2096
+ setAspect(void 0);
2097
+ setImageLoaded(false);
2098
+ naturalSize.current = null;
2099
+ }
2100
+ }, [isOpen, imageUrl]);
2101
+ if (!isOpen) return null;
2102
+ const handleImageLoad = (e2) => {
2103
+ const { naturalWidth, naturalHeight } = e2.currentTarget;
2104
+ if (naturalWidth <= 0 || naturalHeight <= 0) return;
2105
+ naturalSize.current = { width: naturalWidth, height: naturalHeight };
2106
+ setImageLoaded(true);
2107
+ setCrop(
2108
+ initialTrim ? trimToPercentCrop(initialTrim, naturalWidth, naturalHeight) : centeredCrop(naturalWidth, naturalHeight, void 0)
2109
+ );
2110
+ };
2111
+ const handleAspectChange = (next) => {
2112
+ setAspect(next);
2113
+ const size = naturalSize.current;
2114
+ if (size) setCrop(centeredCrop(size.width, size.height, next));
2115
+ };
2116
+ const handleApply = () => {
2117
+ const size = naturalSize.current;
2118
+ if (!size || !imageLoaded || !crop || crop.width <= 0 || crop.height <= 0) return;
2119
+ const left = Math.min(Math.max(crop.x / 100 * size.width, 0), size.width - 1);
2120
+ const top = Math.min(Math.max(crop.y / 100 * size.height, 0), size.height - 1);
2121
+ onApply({
2122
+ left,
2123
+ top,
2124
+ width: Math.min(crop.width / 100 * size.width, size.width - left),
2125
+ height: Math.min(crop.height / 100 * size.height, size.height - top)
2126
+ });
2127
+ };
2128
+ const chipStyle = (active) => ({
2129
+ padding: "3px 10px",
2130
+ fontSize: "12px",
2131
+ fontWeight: 500,
2132
+ border: "1px solid " + (active ? "#2563eb" : "#d0d0d0"),
2133
+ borderRadius: "4px",
2134
+ cursor: "pointer",
2135
+ fontFamily: "inherit",
2136
+ backgroundColor: active ? "#2563eb" : "white",
2137
+ color: active ? "white" : "#444"
2138
+ });
2139
+ return /* @__PURE__ */ jsx9(
2140
+ "div",
2141
+ {
2142
+ style: {
2143
+ position: "fixed",
2144
+ top: 0,
2145
+ left: 0,
2146
+ right: 0,
2147
+ bottom: 0,
2148
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
2149
+ display: "flex",
2150
+ alignItems: "center",
2151
+ justifyContent: "center",
2152
+ zIndex: 99999,
2153
+ padding: "20px"
2154
+ },
2155
+ onClick: onClose,
2156
+ children: /* @__PURE__ */ jsxs8(
2157
+ "div",
2158
+ {
2159
+ style: {
2160
+ backgroundColor: "white",
2161
+ borderRadius: "12px",
2162
+ boxShadow: "0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04)",
2163
+ maxWidth: "720px",
2164
+ width: "100%",
2165
+ display: "flex",
2166
+ flexDirection: "column",
2167
+ overflow: "hidden"
2168
+ },
2169
+ onClick: (e2) => e2.stopPropagation(),
2170
+ children: [
2171
+ /* @__PURE__ */ jsxs8(
2172
+ "div",
2173
+ {
2174
+ style: {
2175
+ display: "flex",
2176
+ alignItems: "center",
2177
+ justifyContent: "space-between",
2178
+ padding: "16px 20px",
2179
+ borderBottom: "1px solid #eee"
2180
+ },
2181
+ children: [
2182
+ /* @__PURE__ */ jsx9("div", { style: { fontSize: "16px", fontWeight: 600 }, children: "Crop image" }),
2183
+ /* @__PURE__ */ jsx9(
2184
+ "button",
2185
+ {
2186
+ type: "button",
2187
+ onClick: onClose,
2188
+ "aria-label": "Close",
2189
+ style: {
2190
+ border: "none",
2191
+ background: "none",
2192
+ fontSize: "18px",
2193
+ cursor: "pointer",
2194
+ color: "#666",
2195
+ lineHeight: 1
2196
+ },
2197
+ children: "\xD7"
2198
+ }
2199
+ )
2200
+ ]
2201
+ }
2202
+ ),
2203
+ /* @__PURE__ */ jsx9("div", { style: { padding: "16px 20px", display: "flex", gap: "6px", flexWrap: "wrap" }, children: ASPECT_PRESETS.map((preset) => /* @__PURE__ */ jsx9(
2204
+ "button",
2205
+ {
2206
+ type: "button",
2207
+ onClick: () => handleAspectChange(preset.value),
2208
+ style: chipStyle(aspect === preset.value),
2209
+ children: preset.label
2210
+ },
2211
+ preset.label
2212
+ )) }),
2213
+ /* @__PURE__ */ jsx9(
2214
+ "div",
2215
+ {
2216
+ style: {
2217
+ padding: "0 20px",
2218
+ display: "flex",
2219
+ justifyContent: "center",
2220
+ overflow: "auto",
2221
+ maxHeight: "55vh"
2222
+ },
2223
+ children: /* @__PURE__ */ jsx9(
2224
+ h,
2225
+ {
2226
+ crop,
2227
+ onChange: (_, percentCrop) => setCrop(percentCrop),
2228
+ aspect,
2229
+ keepSelection: true,
2230
+ children: /* @__PURE__ */ jsx9(
2231
+ "img",
2232
+ {
2233
+ src: imageUrl,
2234
+ onLoad: handleImageLoad,
2235
+ onError: () => setImageLoaded(false),
2236
+ alt: "Image to crop",
2237
+ style: { maxWidth: "100%", maxHeight: "55vh", display: "block" }
2238
+ }
2239
+ )
2240
+ }
2241
+ )
2242
+ }
2243
+ ),
2244
+ /* @__PURE__ */ jsxs8(
2245
+ "div",
2246
+ {
2247
+ style: {
2248
+ display: "flex",
2249
+ justifyContent: "flex-end",
2250
+ gap: "8px",
2251
+ padding: "16px 20px"
2252
+ },
2253
+ children: [
2254
+ /* @__PURE__ */ jsx9(
2255
+ "button",
2256
+ {
2257
+ type: "button",
2258
+ onClick: onClose,
2259
+ style: {
2260
+ padding: "6px 14px",
2261
+ backgroundColor: "white",
2262
+ color: "#666",
2263
+ border: "1px solid #d0d0d0",
2264
+ borderRadius: "4px",
2265
+ fontSize: "13px",
2266
+ fontWeight: 500,
2267
+ cursor: "pointer",
2268
+ fontFamily: "inherit"
2269
+ },
2270
+ children: "Cancel"
2271
+ }
2272
+ ),
2273
+ /* @__PURE__ */ jsx9(
2274
+ "button",
2275
+ {
2276
+ type: "button",
2277
+ onClick: handleApply,
2278
+ disabled: !imageLoaded || !crop || crop.width <= 0 || crop.height <= 0,
2279
+ style: {
2280
+ padding: "6px 14px",
2281
+ backgroundColor: "#2563eb",
2282
+ color: "white",
2283
+ border: "none",
2284
+ borderRadius: "4px",
2285
+ fontSize: "13px",
2286
+ fontWeight: 500,
2287
+ cursor: "pointer",
2288
+ fontFamily: "inherit"
2289
+ },
2290
+ children: "Apply crop"
2291
+ }
2292
+ )
2293
+ ]
2294
+ }
2295
+ )
2296
+ ]
2297
+ }
2298
+ )
2299
+ }
2300
+ );
2301
+ }
2302
+
2303
+ // src/media-value.ts
2304
+ var STRUCTURAL_MEDIA_KEYS = /* @__PURE__ */ new Set([
2305
+ "assetId",
2306
+ "versionId",
2307
+ "url",
2308
+ "metaSchemaVersion"
2309
+ ]);
2310
+ function isMediaValue(value) {
2311
+ return typeof value === "object" && value !== null;
2312
+ }
2313
+ function makeMediaValue(input) {
2314
+ if (!input.assetId || !input.versionId) {
2315
+ return input.url;
2316
+ }
2317
+ const value = {
2318
+ assetId: input.assetId,
2319
+ versionId: input.versionId,
2320
+ url: input.url
2321
+ };
2322
+ if (typeof input.metaSchemaVersion === "number") {
2323
+ value.metaSchemaVersion = input.metaSchemaVersion;
2324
+ }
2325
+ if (input.metadata) {
2326
+ for (const [k, v] of Object.entries(input.metadata)) {
2327
+ if (v === void 0 || v === "") continue;
2328
+ if (STRUCTURAL_MEDIA_KEYS.has(k)) continue;
2329
+ value[k] = v;
2330
+ }
2331
+ }
2332
+ return value;
2333
+ }
2334
+ function buildValueFromAsset(asset, crop) {
2335
+ const metadata = { ...asset.metadata ?? {} };
2336
+ if (typeof asset.width === "number") metadata.width = asset.width;
2337
+ if (typeof asset.height === "number") metadata.height = asset.height;
2338
+ return makeMediaValue({
2339
+ assetId: asset.assetId,
2340
+ versionId: asset.versionId,
2341
+ url: buildValueWithCrop(getBaseUrl(asset.url), crop),
2342
+ metaSchemaVersion: asset.metaSchemaVersion,
2343
+ metadata
2344
+ });
2345
+ }
2346
+ function applyCropToValue(value, crop) {
2347
+ const currentUrl = isMediaValue(value) ? value.url : value;
2348
+ const baseUrl = getBaseUrl(typeof currentUrl === "string" ? currentUrl : "");
2349
+ if (!baseUrl) return value;
2350
+ const url = buildValueWithCrop(baseUrl, crop);
2351
+ return isMediaValue(value) ? { ...value, url } : url;
2352
+ }
2353
+ function applyTrimToValue(value, rect) {
2354
+ const currentUrl = isMediaValue(value) ? value.url : value;
2355
+ const baseUrl = getBaseUrl(typeof currentUrl === "string" ? currentUrl : "");
2356
+ if (!baseUrl) return value;
2357
+ const url = buildValueWithTrim(baseUrl, rect);
2358
+ return isMediaValue(value) ? { ...value, url } : url;
2359
+ }
2360
+ function setMetaOnValue(value, fieldName, fieldValue) {
2361
+ if (!isMediaValue(value)) return value;
2362
+ if (STRUCTURAL_MEDIA_KEYS.has(fieldName)) return value;
2363
+ return { ...value, [fieldName]: fieldValue };
2364
+ }
2365
+
2366
+ // src/components/media-object-field.tsx
2367
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
2368
+ function MediaObjectFieldRender(props) {
2369
+ const { value, onChange, readOnly } = props;
2370
+ const [isLibraryOpen, setIsLibraryOpen] = useState7(false);
2371
+ const [isCropOpen, setIsCropOpen] = useState7(false);
2372
+ const schema = orderAltFirst(useMediaSchema());
2373
+ const isObject = isMediaValue(value);
2374
+ const urlString = isObject ? value.url : typeof value === "string" ? value : "";
2375
+ const baseUrl = getBaseUrl(urlString);
2376
+ const cropMode = getCropMode(urlString);
2377
+ const handleCropChange = (crop) => {
2378
+ if (!baseUrl) return;
2379
+ onChange(applyCropToValue(value, crop));
2380
+ };
2381
+ const handleApplyTrim = (rect) => {
2382
+ onChange(applyTrimToValue(value, rect));
2383
+ setIsCropOpen(false);
2384
+ };
2385
+ const handleMetaChange = (fieldName, fieldValue) => {
2386
+ onChange(setMetaOnValue(value, fieldName, fieldValue));
2387
+ };
2388
+ const handleSelectItem = (item) => {
2389
+ onChange(buildValueFromAsset(item, cropMode));
2390
+ setIsLibraryOpen(false);
2391
+ };
2392
+ const buttonBase = {
2393
+ padding: "3px 10px",
2394
+ fontSize: "12px",
2395
+ fontWeight: 500,
2396
+ border: "1px solid #d0d0d0",
2397
+ borderRadius: "4px",
2398
+ cursor: readOnly ? "not-allowed" : "pointer",
2399
+ fontFamily: "inherit"
2400
+ };
2401
+ const inputStyle = {
2402
+ width: "100%",
2403
+ padding: "6px 8px",
2404
+ border: "1px solid #d0d0d0",
2405
+ borderRadius: "4px",
2406
+ fontSize: "13px",
2407
+ fontFamily: "inherit",
2408
+ boxSizing: "border-box",
2409
+ outline: "none"
2410
+ };
2411
+ return /* @__PURE__ */ jsxs9("div", { style: { width: "100%" }, children: [
2412
+ baseUrl ? /* @__PURE__ */ jsx10("div", { style: { marginBottom: "8px" }, children: /* @__PURE__ */ jsx10(
2413
+ "img",
2414
+ {
2415
+ src: urlString,
2416
+ alt: "Preview",
2417
+ style: {
2418
+ width: "100%",
2419
+ maxHeight: "120px",
2420
+ objectFit: "cover",
2421
+ borderRadius: "6px",
2422
+ display: "block",
2423
+ border: "1px solid #e0e0e0"
2424
+ }
2425
+ }
2426
+ ) }) : /* @__PURE__ */ jsx10(
2427
+ "div",
2428
+ {
2429
+ style: {
2430
+ height: "80px",
2431
+ border: "2px dashed #d0d0d0",
2432
+ borderRadius: "6px",
2433
+ display: "flex",
2434
+ alignItems: "center",
2435
+ justifyContent: "center",
2436
+ marginBottom: "8px",
2437
+ color: "#999",
2438
+ fontSize: "13px",
2439
+ cursor: readOnly ? "default" : "pointer"
2440
+ },
2441
+ onClick: () => !readOnly && setIsLibraryOpen(true),
2442
+ children: "Click to select image"
2443
+ }
2444
+ ),
2445
+ baseUrl && /* @__PURE__ */ jsxs9("div", { style: { marginBottom: "8px" }, children: [
2446
+ /* @__PURE__ */ jsx10("div", { style: { fontSize: "11px", color: "#666", fontWeight: 500, marginBottom: "4px" }, children: "Crop" }),
2447
+ /* @__PURE__ */ jsxs9("div", { style: { display: "flex", gap: "4px" }, children: [
2448
+ /* @__PURE__ */ jsx10(
2449
+ "button",
2450
+ {
2451
+ type: "button",
2452
+ disabled: readOnly,
2453
+ onClick: () => handleCropChange("fit"),
2454
+ style: {
2455
+ ...buttonBase,
2456
+ backgroundColor: cropMode === "fit" ? "#2563eb" : "white",
2457
+ color: cropMode === "fit" ? "white" : "#444",
2458
+ borderColor: cropMode === "fit" ? "#2563eb" : "#d0d0d0",
2459
+ opacity: readOnly ? 0.5 : 1
2460
+ },
2461
+ children: "Fit in"
2462
+ }
2463
+ ),
2464
+ /* @__PURE__ */ jsx10(
2465
+ "button",
2466
+ {
2467
+ type: "button",
2468
+ disabled: readOnly,
2469
+ onClick: () => handleCropChange("smart"),
2470
+ style: {
2471
+ ...buttonBase,
2472
+ backgroundColor: cropMode === "smart" ? "#2563eb" : "white",
2473
+ color: cropMode === "smart" ? "white" : "#444",
2474
+ borderColor: cropMode === "smart" ? "#2563eb" : "#d0d0d0",
2475
+ opacity: readOnly ? 0.5 : 1
2476
+ },
2477
+ children: "Smart crop"
2478
+ }
2479
+ ),
2480
+ /* @__PURE__ */ jsx10(
2481
+ "button",
2482
+ {
2483
+ type: "button",
2484
+ disabled: readOnly,
2485
+ onClick: () => setIsCropOpen(true),
2486
+ style: {
2487
+ ...buttonBase,
2488
+ backgroundColor: cropMode === "custom" ? "#2563eb" : "white",
2489
+ color: cropMode === "custom" ? "white" : "#444",
2490
+ borderColor: cropMode === "custom" ? "#2563eb" : "#d0d0d0",
2491
+ opacity: readOnly ? 0.5 : 1
2492
+ },
2493
+ children: "Custom\u2026"
2494
+ }
2495
+ )
2496
+ ] })
2497
+ ] }),
2498
+ baseUrl && /* @__PURE__ */ jsxs9("div", { style: { marginBottom: "8px" }, children: [
2499
+ !isObject && /* @__PURE__ */ jsx10("div", { style: { fontSize: "11px", color: "#a15c00", marginBottom: "6px" }, children: "Select from the library to add metadata." }),
2500
+ schema.map((f2) => {
2501
+ const current = isObject && typeof value[f2.name] === "string" ? value[f2.name] : "";
2502
+ return /* @__PURE__ */ jsxs9("div", { style: { marginBottom: "8px" }, children: [
2503
+ /* @__PURE__ */ jsxs9(
2504
+ "div",
2505
+ {
2506
+ style: {
2507
+ fontSize: "11px",
2508
+ color: "#666",
2509
+ fontWeight: 500,
2510
+ marginBottom: "4px"
2511
+ },
2512
+ children: [
2513
+ f2.label,
2514
+ f2.required ? " *" : ""
2515
+ ]
2516
+ }
2517
+ ),
2518
+ /* @__PURE__ */ jsx10(
2519
+ "input",
2520
+ {
2521
+ type: "text",
2522
+ value: current,
2523
+ disabled: readOnly || !isObject,
2524
+ placeholder: !isObject ? "Re-select image to edit" : "",
2525
+ onChange: (e2) => handleMetaChange(f2.name, e2.target.value),
2526
+ style: { ...inputStyle, opacity: readOnly || !isObject ? 0.6 : 1 }
2527
+ }
2528
+ )
2529
+ ] }, f2.name);
2530
+ })
2531
+ ] }),
2532
+ /* @__PURE__ */ jsxs9("div", { style: { display: "flex", gap: "6px" }, children: [
2533
+ /* @__PURE__ */ jsx10(
2534
+ "button",
2535
+ {
2536
+ type: "button",
2537
+ onClick: () => setIsLibraryOpen(true),
2538
+ disabled: readOnly,
2539
+ style: {
2540
+ flex: 1,
2541
+ padding: "6px 12px",
2542
+ backgroundColor: "#2563eb",
2543
+ color: "white",
2544
+ border: "none",
2545
+ borderRadius: "4px",
2546
+ fontSize: "13px",
2547
+ fontWeight: 500,
2548
+ cursor: readOnly ? "not-allowed" : "pointer",
2549
+ opacity: readOnly ? 0.5 : 1,
2550
+ fontFamily: "inherit"
2551
+ },
2552
+ children: "Choose from Library"
2553
+ }
2554
+ ),
2555
+ baseUrl && /* @__PURE__ */ jsx10(
2556
+ "button",
2557
+ {
2558
+ type: "button",
2559
+ onClick: () => onChange(""),
2560
+ disabled: readOnly,
2561
+ style: {
2562
+ padding: "6px 12px",
2563
+ backgroundColor: "white",
2564
+ color: "#666",
2565
+ border: "1px solid #d0d0d0",
2566
+ borderRadius: "4px",
2567
+ fontSize: "13px",
2568
+ fontWeight: 500,
2569
+ cursor: readOnly ? "not-allowed" : "pointer",
2570
+ opacity: readOnly ? 0.5 : 1,
2571
+ fontFamily: "inherit"
2572
+ },
2573
+ children: "Clear"
2574
+ }
2575
+ )
2576
+ ] }),
2577
+ /* @__PURE__ */ jsx10(
2578
+ MediaLibrary,
2579
+ {
2580
+ isOpen: isLibraryOpen,
2581
+ onClose: () => setIsLibraryOpen(false),
2582
+ onSelect: () => {
2583
+ },
2584
+ onSelectItem: handleSelectItem
2585
+ }
2586
+ ),
2587
+ /* @__PURE__ */ jsx10(
2588
+ CropDialog,
2589
+ {
2590
+ isOpen: isCropOpen,
2591
+ imageUrl: baseUrl,
2592
+ initialTrim: getTrimRect(urlString),
2593
+ onApply: handleApplyTrim,
2594
+ onClose: () => setIsCropOpen(false)
2595
+ }
2596
+ )
2597
+ ] });
2598
+ }
2599
+
2600
+ // src/puck-css-bridge.tsx
2601
+ import { useContext as useContext2, useMemo } from "react";
2602
+ import { P1PuckContext, useP1Auth } from "@pantheon-systems/puck-css";
2603
+ import { jsx as jsx11 } from "react/jsx-runtime";
2604
+ var DEFAULT_WORKER_URL = "https://media.p1.pantheon.io";
2605
+ function resolveSiteId(explicit, contextSiteId) {
2606
+ const siteId = explicit ?? contextSiteId;
2607
+ if (!siteId) {
2608
+ throw new Error(
2609
+ "p1-media: siteId is required. Pass it explicitly to createMediaPlugin(), or render the plugin inside a puck-css P1PuckProvider so it can be read from context."
2610
+ );
2611
+ }
2612
+ return siteId;
2613
+ }
2614
+ function resolveGetAuthToken(explicit, contextGetToken) {
2615
+ const getAuthToken = explicit ?? contextGetToken;
2616
+ if (!getAuthToken) {
2617
+ throw new Error(
2618
+ "p1-media: getAuthToken is required. Pass it explicitly to createMediaPlugin(), or render the plugin inside a puck-css P1AuthProvider so it can be read from context."
2619
+ );
2620
+ }
2621
+ return getAuthToken;
2622
+ }
2623
+ function useAmbientSiteId() {
2624
+ return useContext2(P1PuckContext)?.siteId;
2625
+ }
2626
+ function useAmbientGetAuthToken() {
2627
+ try {
2628
+ return useP1Auth().getToken;
2629
+ } catch {
2630
+ return void 0;
2631
+ }
2632
+ }
2633
+ function buildMediaConfig(options, ambient = {}) {
2634
+ return {
2635
+ workerUrl: options.workerUrl ?? DEFAULT_WORKER_URL,
2636
+ siteId: resolveSiteId(options.siteId, ambient.siteId),
2637
+ workstreamId: options.workstreamId,
2638
+ getAuthToken: resolveGetAuthToken(options.getAuthToken, ambient.getAuthToken),
2639
+ metadataFields: options.metadataFields
2640
+ };
2641
+ }
2642
+ function MediaConfigResolver({
2643
+ options,
2644
+ children
2645
+ }) {
2646
+ const ambientSiteId = useAmbientSiteId();
2647
+ const ambientGetAuthToken = useAmbientGetAuthToken();
2648
+ const config = useMemo(
2649
+ () => buildMediaConfig(options, { siteId: ambientSiteId, getAuthToken: ambientGetAuthToken }),
2650
+ [options, ambientSiteId, ambientGetAuthToken]
2651
+ );
2652
+ return /* @__PURE__ */ jsx11(MediaConfigProvider, { config, children });
2653
+ }
2654
+
2655
+ // src/plugin.tsx
2656
+ import { Fragment as Fragment3, jsx as jsx12 } from "react/jsx-runtime";
2657
+ function createMediaPlugin(options) {
2658
+ const patterns = options.fieldNamePatterns ?? DEFAULT_MEDIA_PATTERNS;
2659
+ return {
2660
+ name: "p1-media",
2661
+ overrides: {
2662
+ fieldTypes: {
2663
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2664
+ text: (props) => {
2665
+ const { children, name, field, value, onChange, readOnly, id } = props;
2666
+ const bareFieldName = name?.split(".").pop() ?? name;
2667
+ const isMediaField = patterns.some((p2) => p2.test(bareFieldName));
2668
+ if (!isMediaField) {
2669
+ return /* @__PURE__ */ jsx12(Fragment3, { children });
2670
+ }
2671
+ return /* @__PURE__ */ jsx12(MediaConfigResolver, { options, children: /* @__PURE__ */ jsx12(
2672
+ MediaFieldRender,
2673
+ {
2674
+ field: {
2675
+ type: "custom",
2676
+ label: field?.label ?? name,
2677
+ render: () => /* @__PURE__ */ jsx12(Fragment3, {})
2678
+ },
2679
+ name,
2680
+ id: id ?? name,
2681
+ value: value ?? "",
2682
+ onChange,
2683
+ readOnly
2684
+ }
2685
+ ) });
2686
+ },
2687
+ // Rich mode: a first-class `p1-media` field whose value is an object.
2688
+ // Puck ≥0.20 dispatches overrides.fieldTypes[field.type] for new types.
2689
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2690
+ "p1-media": (props) => {
2691
+ const { name, field, value, onChange, readOnly, id } = props;
2692
+ return /* @__PURE__ */ jsx12(MediaConfigResolver, { options, children: /* @__PURE__ */ jsx12(
2693
+ MediaObjectFieldRender,
2694
+ {
2695
+ label: field?.label ?? name,
2696
+ name,
2697
+ id: id ?? name,
2698
+ value: value ?? "",
2699
+ onChange,
2700
+ readOnly
2701
+ }
2702
+ ) });
2703
+ }
2704
+ }
2705
+ },
2706
+ // Editor-preview only (never written back): normalize a legacy string to
2707
+ // the object shape so components reading the raw prop see `{ url, alt }`.
2708
+ // The R10 write-path guard lives in makeMediaValue, not here.
2709
+ fieldTransforms: {
2710
+ "p1-media": ({ value }) => typeof value === "string" ? { url: value, alt: "" } : value
2711
+ }
2712
+ };
2713
+ }
2714
+
2715
+ // src/utils.ts
2716
+ function buildImageUrl(url, params) {
2717
+ if (!url) return url;
2718
+ const parsed = new URL(url);
2719
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return url;
2720
+ for (const [k, v] of Object.entries(params)) {
2721
+ if (v !== void 0) {
2722
+ parsed.searchParams.set(k, String(v));
2723
+ }
2724
+ }
2725
+ return parsed.toString();
2726
+ }
2727
+
2728
+ // src/render.tsx
2729
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
2730
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
2731
+ function validateSrc(url, mediaBaseUrl) {
2732
+ if (!url || !mediaBaseUrl) return "";
2733
+ let parsed;
2734
+ let base;
2735
+ try {
2736
+ parsed = new URL(url);
2737
+ base = new URL(mediaBaseUrl);
2738
+ } catch {
2739
+ return "";
2740
+ }
2741
+ if (parsed.origin !== base.origin) return "";
2742
+ if (parsed.protocol !== "https:") {
2743
+ const isLocalDevBase = base.protocol === "http:" && LOOPBACK_HOSTS.has(base.hostname);
2744
+ if (!isLocalDevBase || parsed.protocol !== "http:") return "";
2745
+ }
2746
+ return url;
2747
+ }
2748
+ function getMediaProps(value, options) {
2749
+ const mediaBaseUrl = options?.mediaBaseUrl;
2750
+ const transform = options?.transform;
2751
+ const finalize = (rawUrl) => {
2752
+ const validated = validateSrc(rawUrl, mediaBaseUrl);
2753
+ return validated && transform ? buildImageUrl(validated, transform) : validated;
2754
+ };
2755
+ if (value == null) return { src: "", alt: "" };
2756
+ if (typeof value === "string") {
2757
+ return { src: finalize(value), alt: "" };
2758
+ }
2759
+ const props = {
2760
+ src: finalize(typeof value.url === "string" ? value.url : ""),
2761
+ alt: typeof value.alt === "string" ? value.alt : ""
2762
+ };
2763
+ if (typeof value.width === "number") props.width = value.width;
2764
+ if (typeof value.height === "number") props.height = value.height;
2765
+ return props;
2766
+ }
2767
+ function MediaImage({
2768
+ image,
2769
+ mediaBaseUrl,
2770
+ transform,
2771
+ alt,
2772
+ ...rest
2773
+ }) {
2774
+ const props = getMediaProps(image, { mediaBaseUrl, transform });
2775
+ if (!props.src) return null;
2776
+ return /* @__PURE__ */ jsx13(
2777
+ "img",
2778
+ {
2779
+ src: props.src,
2780
+ alt: alt ?? props.alt,
2781
+ ...props.width !== void 0 ? { width: props.width } : {},
2782
+ ...props.height !== void 0 ? { height: props.height } : {},
2783
+ ...rest
2784
+ }
2785
+ );
2786
+ }
2787
+ function collectFigureFields(image, schema) {
2788
+ if (!isMediaValue(image)) return [];
2789
+ const out = [];
2790
+ if (schema && schema.length > 0) {
2791
+ for (const entry of schema) {
2792
+ if (entry.name === "alt") continue;
2793
+ const raw = image[entry.name];
2794
+ if (raw === void 0 || raw === "") continue;
2795
+ out.push({ name: entry.name, label: entry.label, value: String(raw) });
2796
+ }
2797
+ } else {
2798
+ for (const [k, v] of Object.entries(image)) {
2799
+ if (k === "alt") continue;
2800
+ if (STRUCTURAL_MEDIA_KEYS.has(k)) continue;
2801
+ if (typeof v !== "string" || v === "") continue;
2802
+ out.push({ name: k, value: v });
2803
+ }
2804
+ }
2805
+ return out;
2806
+ }
2807
+ function MediaFigure({
2808
+ image,
2809
+ schema,
2810
+ mediaBaseUrl,
2811
+ transform,
2812
+ className,
2813
+ captionClassName
2814
+ }) {
2815
+ const props = getMediaProps(image, { mediaBaseUrl, transform });
2816
+ if (!props.src) return null;
2817
+ const fields = collectFigureFields(image, schema);
2818
+ return /* @__PURE__ */ jsxs10("figure", { className, children: [
2819
+ /* @__PURE__ */ jsx13(
2820
+ "img",
2821
+ {
2822
+ src: props.src,
2823
+ alt: props.alt,
2824
+ ...props.width !== void 0 ? { width: props.width } : {},
2825
+ ...props.height !== void 0 ? { height: props.height } : {}
2826
+ }
2827
+ ),
2828
+ fields.length > 0 && /* @__PURE__ */ jsx13("figcaption", { className: captionClassName, children: fields.map((f2) => /* @__PURE__ */ jsx13("span", { "data-field": f2.name, children: f2.value }, f2.name)) })
2829
+ ] });
2830
+ }
2831
+
2832
+ // src/media-figure-block.tsx
2833
+ import { jsx as jsx14 } from "react/jsx-runtime";
2834
+ var DEFAULT_TRANSFORM = { width: 1200, height: 630, format: "auto" };
2835
+ var DEFAULT_MEDIA_BASE_URL = "https://media.p1.pantheon.io";
2836
+ var placeholderStyle = {
2837
+ display: "flex",
2838
+ alignItems: "center",
2839
+ justifyContent: "center",
2840
+ minHeight: "200px",
2841
+ borderRadius: "8px",
2842
+ backgroundColor: "#e5e5e5",
2843
+ color: "#525252",
2844
+ fontSize: "14px"
2845
+ };
2846
+ function createMediaFigureBlock(options) {
2847
+ const {
2848
+ mediaBaseUrl = DEFAULT_MEDIA_BASE_URL,
2849
+ transform = DEFAULT_TRANSFORM,
2850
+ label = "Media Figure",
2851
+ fieldLabel = "Photo",
2852
+ schema,
2853
+ className,
2854
+ captionClassName,
2855
+ placeholder = "Choose a photo from the media library"
2856
+ } = options;
2857
+ return {
2858
+ label,
2859
+ fields: {
2860
+ // `p1-media` is registered by createMediaPlugin via overrides.fieldTypes,
2861
+ // so it is not part of Puck's built-in Field union.
2862
+ photo: { type: "p1-media", label: fieldLabel }
2863
+ },
2864
+ defaultProps: {
2865
+ photo: null
2866
+ },
2867
+ render: ({ photo }) => {
2868
+ const { src } = getMediaProps(photo ?? null, { mediaBaseUrl });
2869
+ if (!src) {
2870
+ return /* @__PURE__ */ jsx14("div", { style: placeholderStyle, children: placeholder });
2871
+ }
2872
+ return /* @__PURE__ */ jsx14(
2873
+ MediaFigure,
2874
+ {
2875
+ image: photo ?? null,
2876
+ mediaBaseUrl,
2877
+ transform,
2878
+ schema,
2879
+ className,
2880
+ captionClassName
2881
+ }
2882
+ );
2883
+ }
2884
+ };
2885
+ }
2886
+ export {
2887
+ DEFAULT_MEDIA_PATTERNS,
2888
+ MediaFigure,
2889
+ MediaImage,
2890
+ buildImageUrl,
2891
+ createMediaFigureBlock,
2892
+ createMediaPlugin,
2893
+ getMediaProps,
2894
+ isMediaValue,
2895
+ makeMediaValue
2896
+ };
2897
+ //# sourceMappingURL=index.mjs.map