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