@ttt-productions/file-input 0.11.15 → 0.11.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/index.d.ts +4 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +4 -4
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/image-utils.d.ts +3 -3
  6. package/dist/lib/image-utils.js +10 -10
  7. package/dist/lib/image-utils.js.map +1 -1
  8. package/dist/lib/infer-content-type.d.ts +1 -1
  9. package/dist/lib/infer-content-type.js +29 -29
  10. package/dist/lib/infer-content-type.js.map +1 -1
  11. package/dist/lib/read-media-meta.d.ts +1 -1
  12. package/dist/lib/read-media-meta.d.ts.map +1 -1
  13. package/dist/lib/read-media-meta.js +20 -13
  14. package/dist/lib/read-media-meta.js.map +1 -1
  15. package/dist/lib/validate-media-duration.js +3 -3
  16. package/dist/lib/validate-media-duration.js.map +1 -1
  17. package/dist/lib/validate-upload-kind.d.ts.map +1 -1
  18. package/dist/lib/validate-upload-kind.js.map +1 -1
  19. package/dist/progress-bar.d.ts +2 -0
  20. package/dist/progress-bar.d.ts.map +1 -0
  21. package/dist/progress-bar.js +2 -0
  22. package/dist/progress-bar.js.map +1 -0
  23. package/dist/react/components/audio-input.d.ts +2 -2
  24. package/dist/react/components/audio-input.js +2 -2
  25. package/dist/react/components/audio-input.js.map +1 -1
  26. package/dist/react/components/file-input.d.ts +1 -1
  27. package/dist/react/components/file-input.d.ts.map +1 -1
  28. package/dist/react/components/file-input.js +59 -42
  29. package/dist/react/components/file-input.js.map +1 -1
  30. package/dist/react/components/image-cropper-modal.d.ts +3 -3
  31. package/dist/react/components/image-cropper-modal.d.ts.map +1 -1
  32. package/dist/react/components/image-cropper-modal.js +6 -6
  33. package/dist/react/components/image-cropper-modal.js.map +1 -1
  34. package/dist/react/components/image-input.d.ts +2 -2
  35. package/dist/react/components/image-input.js +2 -2
  36. package/dist/react/components/image-input.js.map +1 -1
  37. package/dist/react/components/media-constraints-hint.d.ts +1 -1
  38. package/dist/react/components/media-constraints-hint.d.ts.map +1 -1
  39. package/dist/react/components/media-constraints-hint.js +26 -24
  40. package/dist/react/components/media-constraints-hint.js.map +1 -1
  41. package/dist/react/components/media-input.d.ts +2 -2
  42. package/dist/react/components/media-input.d.ts.map +1 -1
  43. package/dist/react/components/media-input.js +107 -89
  44. package/dist/react/components/media-input.js.map +1 -1
  45. package/dist/react/components/photo-capture-modal.d.ts +1 -1
  46. package/dist/react/components/photo-capture-modal.d.ts.map +1 -1
  47. package/dist/react/components/photo-capture-modal.js +15 -15
  48. package/dist/react/components/photo-capture-modal.js.map +1 -1
  49. package/dist/react/components/record-dialog.d.ts +3 -3
  50. package/dist/react/components/record-dialog.d.ts.map +1 -1
  51. package/dist/react/components/record-dialog.js +49 -50
  52. package/dist/react/components/record-dialog.js.map +1 -1
  53. package/dist/react/components/video-input.d.ts +2 -2
  54. package/dist/react/components/video-input.js +2 -2
  55. package/dist/react/components/video-input.js.map +1 -1
  56. package/dist/react/index.d.ts +8 -8
  57. package/dist/react/index.js +8 -8
  58. package/dist/react/index.js.map +1 -1
  59. package/dist/types.d.ts +3 -3
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/types.js.map +1 -1
  62. package/package.json +4 -4
@@ -1,20 +1,20 @@
1
- "use client";
1
+ 'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useMemo, useRef, useState } from "react";
4
- import { getSimplifiedMediaType } from "@ttt-productions/media-schemas";
5
- import { Info, Camera, Mic, Video, Upload, X, Plus } from "lucide-react";
6
- import { Alert, AlertDescription, Button, Card, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Input, Progress, Spinner, } from "@ttt-productions/ui-core/react";
7
- import { cn } from "@ttt-productions/ui-core";
8
- import { MediaPreview } from "@ttt-productions/media-viewer/react";
9
- import { DEFAULT_PROGRESS_BAR_MIN_BYTES } from "../../index.js";
10
- import { RecordDialog } from "./record-dialog.js";
11
- import { ensureFileWithContentType } from "../../lib/infer-content-type.js";
12
- import { NEUTRAL_CONTENT_TYPE, projectAcceptTokens } from "@ttt-productions/media-schemas";
13
- import { ImageCropperModal } from "./image-cropper-modal.js";
14
- import { MediaConstraintsHint } from "./media-constraints-hint.js";
15
- import { PhotoCaptureModal } from "./photo-capture-modal.js";
16
- import { readMediaMeta } from "../../lib/read-media-meta.js";
17
- import { validateMediaDuration } from "../../lib/validate-media-duration.js";
3
+ import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useMemo, useRef, useState, } from 'react';
4
+ import { getSimplifiedMediaType } from '@ttt-productions/media-schemas';
5
+ import { Info, Camera, Mic, Video, Upload, X, Plus } from 'lucide-react';
6
+ import { Alert, AlertDescription, Button, Card, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Input, Progress, Spinner, } from '@ttt-productions/ui-core/react';
7
+ import { cn } from '@ttt-productions/ui-core';
8
+ import { MediaPreview } from '@ttt-productions/media-viewer/react';
9
+ import { DEFAULT_PROGRESS_BAR_MIN_BYTES } from '../../progress-bar.js';
10
+ import { RecordDialog } from './record-dialog.js';
11
+ import { ensureFileWithContentType } from '../../lib/infer-content-type.js';
12
+ import { NEUTRAL_CONTENT_TYPE, projectAcceptTokens } from '@ttt-productions/media-schemas';
13
+ import { ImageCropperModal } from './image-cropper-modal.js';
14
+ import { MediaConstraintsHint } from './media-constraints-hint.js';
15
+ import { PhotoCaptureModal } from './photo-capture-modal.js';
16
+ import { readMediaMeta } from '../../lib/read-media-meta.js';
17
+ import { validateMediaDuration } from '../../lib/validate-media-duration.js';
18
18
  /** Built-in advisory shown on EVERY MediaInput's Info affordance (overridable via `helpNotes`,
19
19
  * which appends). Generic + app-agnostic: uploaded files are not a backup/storage service. */
20
20
  const DEFAULT_BACKUP_NOTE = "Keep your own copy of files you upload — this isn't a backup or storage service.";
@@ -32,7 +32,7 @@ function hasConstraints(spec) {
32
32
  spec.maxDurationSec ||
33
33
  procVid?.video?.maxDurationSec ||
34
34
  procAud?.audio?.maxDurationSec ||
35
- (spec.videoOrientation && spec.videoOrientation !== "any") ||
35
+ (spec.videoOrientation && spec.videoOrientation !== 'any') ||
36
36
  spec.requiredAspectRatio ||
37
37
  (procVid?.requiredWidth && procVid?.requiredHeight) ||
38
38
  (procImg?.requiredWidth && procImg?.requiredHeight) ||
@@ -45,9 +45,9 @@ function matchMime(accepted, actual) {
45
45
  const m = actual.trim().toLowerCase();
46
46
  if (!a)
47
47
  return true;
48
- if (a === "*/*")
48
+ if (a === '*/*')
49
49
  return true;
50
- if (a.endsWith("/*"))
50
+ if (a.endsWith('/*'))
51
51
  return m.startsWith(a.slice(0, -1));
52
52
  return a === m;
53
53
  }
@@ -65,7 +65,9 @@ function accepts(spec, file) {
65
65
  const kinds = accept.kinds?.filter(Boolean) ?? [];
66
66
  const mimes = accept.mimes?.filter(Boolean) ?? [];
67
67
  const simplified = getSimplifiedMediaType(file);
68
- const kind = simplified === "image" || simplified === "video" || simplified === "audio" ? simplified : "file";
68
+ const kind = simplified === 'image' || simplified === 'video' || simplified === 'audio'
69
+ ? simplified
70
+ : 'file';
69
71
  const kindOk = kinds.length === 0 ? true : kinds.includes(kind);
70
72
  if (!kindOk)
71
73
  return false;
@@ -80,9 +82,9 @@ function computeAspect(width, height) {
80
82
  return width / height;
81
83
  }
82
84
  function orientationOk(required, actual) {
83
- if (!required || required === "any")
85
+ if (!required || required === 'any')
84
86
  return true;
85
- if (!actual || actual === "any")
87
+ if (!actual || actual === 'any')
86
88
  return true; // treat square/unknown as ok
87
89
  return required === actual;
88
90
  }
@@ -115,7 +117,7 @@ function resolveActiveSpec(entry, kind) {
115
117
  return rest;
116
118
  }
117
119
  export const MediaInput = forwardRef(function MediaInput(props, ref) {
118
- const { spec, cropOverride, disabled = false, isLoading = false, className, buttonLabel = "Add media", clearLabel = "Clear selected file", uploadState, progressBarMinBytes, selectedFile, onClear, onCancel, onBeforeSelect, onChange, helpNotes, } = props;
120
+ const { spec, cropOverride, disabled = false, isLoading = false, className, buttonLabel = 'Add media', clearLabel = 'Clear selected file', uploadState, progressBarMinBytes, selectedFile, onClear, onCancel, onBeforeSelect, onChange, helpNotes, } = props;
119
121
  const id = useId();
120
122
  const pickerRef = useRef(null);
121
123
  const cropSpec = cropOverride ?? spec.imageCrop;
@@ -134,7 +136,7 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
134
136
  const [pendingCropClaim, setPendingCropClaim] = useState(null);
135
137
  const [photoOpen, setPhotoOpen] = useState(false);
136
138
  const [recordOpen, setRecordOpen] = useState(false);
137
- const [recordKind, setRecordKind] = useState("video");
139
+ const [recordKind, setRecordKind] = useState('video');
138
140
  // Controlled state for the multi-action choice dropdown. Human clicks toggle it
139
141
  // via the trigger button; the imperative `openSelection()` handle opens it when
140
142
  // more than one action is enabled.
@@ -173,34 +175,34 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
173
175
  if (formats.length) {
174
176
  const tokens = projectAcceptTokens(formats, spec.accept?.kinds ?? undefined);
175
177
  if (tokens.length)
176
- return tokens.join(", ");
178
+ return tokens.join(', ');
177
179
  }
178
180
  const m = spec.accept?.mimes?.filter(Boolean) ?? [];
179
181
  const k = spec.accept?.kinds?.filter(Boolean) ?? [];
180
182
  if (m.length)
181
- return m.join(", ");
182
- if (k.includes("image") && k.length === 1)
183
- return "image/*";
184
- if (k.includes("video") && k.length === 1)
185
- return "video/*";
186
- if (k.includes("audio") && k.length === 1)
187
- return "audio/*";
183
+ return m.join(', ');
184
+ if (k.includes('image') && k.length === 1)
185
+ return 'image/*';
186
+ if (k.includes('video') && k.length === 1)
187
+ return 'video/*';
188
+ if (k.includes('audio') && k.length === 1)
189
+ return 'audio/*';
188
190
  return undefined; // accept anything
189
191
  }, [spec]);
190
192
  // Simplified kind of the currently-selected file, used to size and type the
191
193
  // inline preview. Null when nothing is selected.
192
194
  const previewKind = useMemo(() => (selectedFile ? getSimplifiedMediaType(selectedFile) : null), [selectedFile]);
193
- const selectedFileLabel = previewKind === "image"
194
- ? "Image selected"
195
- : previewKind === "video"
196
- ? "Video selected"
197
- : previewKind === "audio"
198
- ? "Audio selected"
199
- : "File selected";
195
+ const selectedFileLabel = previewKind === 'image'
196
+ ? 'Image selected'
197
+ : previewKind === 'video'
198
+ ? 'Video selected'
199
+ : previewKind === 'audio'
200
+ ? 'Audio selected'
201
+ : 'File selected';
200
202
  const emit = useCallback((payload) => {
201
203
  // fallback resolution; pre-validation error path when kind is unknown or "file"
202
204
  const rawKind = payload.meta?.kind;
203
- const mediaKind = rawKind === "image" || rawKind === "video" || rawKind === "audio" ? rawKind : "image";
205
+ const mediaKind = rawKind === 'image' || rawKind === 'video' || rawKind === 'audio' ? rawKind : 'image';
204
206
  const resolvedSpec = resolveActiveSpec(spec, mediaKind);
205
207
  if (payload.file)
206
208
  setEmittedSize(payload.file.size);
@@ -216,14 +218,14 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
216
218
  // handling. Recorder/capture files carry their declared kind; a picker
217
219
  // file with unknown metadata passes through NEUTRALLY (never fabricated —
218
220
  // the server inspector owns classification).
219
- const declaredKind = action?.source === "media-recorder" || action?.source === "camera-capture"
220
- ? action.kind === "image" || action.kind === "video" || action.kind === "audio"
221
+ const declaredKind = action?.source === 'media-recorder' || action?.source === 'camera-capture'
222
+ ? action.kind === 'image' || action.kind === 'video' || action.kind === 'audio'
221
223
  ? action.kind
222
224
  : undefined
223
225
  : undefined;
224
226
  const safeFile = ensureFileWithContentType(file, declaredKind);
225
227
  if (!accepts(spec, safeFile)) {
226
- fail(err("invalid_type", "Invalid file type for this upload."));
228
+ fail(err('invalid_type', 'Invalid file type for this upload.'));
227
229
  return;
228
230
  }
229
231
  const meta = await readMediaMeta(safeFile);
@@ -233,12 +235,14 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
233
235
  // known. Never derived from the (already-normalized) MIME for strong
234
236
  // sources; for the picker the meta kind IS the inference.
235
237
  const claim = action ?? {
236
- source: "file-picker",
237
- kind: meta.kind === "image" || meta.kind === "video" || meta.kind === "audio" ? meta.kind : "file",
238
+ source: 'file-picker',
239
+ kind: meta.kind === 'image' || meta.kind === 'video' || meta.kind === 'audio'
240
+ ? meta.kind
241
+ : 'file',
238
242
  };
239
243
  // bytes
240
244
  if (spec.maxBytes && safeFile.size > spec.maxBytes) {
241
- fail(err("too_large", `File too large. Max ${Math.round(spec.maxBytes / 1024 / 1024)}MB.`, {
245
+ fail(err('too_large', `File too large. Max ${Math.round(spec.maxBytes / 1024 / 1024)}MB.`, {
242
246
  size: safeFile.size,
243
247
  maxBytes: spec.maxBytes,
244
248
  }));
@@ -248,15 +252,15 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
248
252
  const maxDur = spec.maxDurationSec ??
249
253
  spec.processing?.video?.video?.maxDurationSec ??
250
254
  spec.processing?.audio?.audio?.maxDurationSec;
251
- if ((meta.kind === "video" || meta.kind === "audio") && maxDur) {
255
+ if ((meta.kind === 'video' || meta.kind === 'audio') && maxDur) {
252
256
  const ok = await validateMediaDuration(safeFile, maxDur);
253
257
  if (!ok) {
254
- fail(err("too_long", `Media too long. Max ${maxDur} seconds.`));
258
+ fail(err('too_long', `Media too long. Max ${maxDur} seconds.`));
255
259
  return;
256
260
  }
257
261
  }
258
262
  // image crop enforcement
259
- if (meta.kind === "image" && cropSpec) {
263
+ if (meta.kind === 'image' && cropSpec) {
260
264
  const reader = new FileReader();
261
265
  reader.onload = () => {
262
266
  setPendingCropFile(safeFile);
@@ -264,12 +268,12 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
264
268
  setCropSrc(reader.result);
265
269
  setCropOpen(true);
266
270
  };
267
- reader.onerror = () => fail(err("read_failed", "Failed to read image for cropping."));
271
+ reader.onerror = () => fail(err('read_failed', 'Failed to read image for cropping.'));
268
272
  reader.readAsDataURL(safeFile);
269
273
  return;
270
274
  }
271
275
  // video uniform enforcement (orientation/aspect/dimensions)
272
- if (meta.kind === "video") {
276
+ if (meta.kind === 'video') {
273
277
  const procVid = spec.processing?.video;
274
278
  const requiredOri = spec.videoOrientation;
275
279
  const requiredAspect = spec.requiredAspectRatio;
@@ -289,21 +293,21 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
289
293
  return;
290
294
  }
291
295
  if (!okOri) {
292
- fail(err("orientation_mismatch", "Video orientation does not match the required format.", {
296
+ fail(err('orientation_mismatch', 'Video orientation does not match the required format.', {
293
297
  requiredOri,
294
298
  actualOri,
295
299
  }));
296
300
  return;
297
301
  }
298
302
  if (!okAspect) {
299
- fail(err("aspect_ratio_mismatch", "Video aspect ratio does not match the required format.", {
303
+ fail(err('aspect_ratio_mismatch', 'Video aspect ratio does not match the required format.', {
300
304
  requiredAspect,
301
305
  actualAspect,
302
306
  }));
303
307
  return;
304
308
  }
305
309
  if (!okDims) {
306
- fail(err("dimensions_mismatch", "Video dimensions do not match the required format.", {
310
+ fail(err('dimensions_mismatch', 'Video dimensions do not match the required format.', {
307
311
  requiredW,
308
312
  requiredH,
309
313
  width: meta.width,
@@ -329,7 +333,7 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
329
333
  }, [validateAndEmit]);
330
334
  const onPick = useCallback(async (e) => {
331
335
  const file = e.target.files?.[0];
332
- e.target.value = "";
336
+ e.target.value = '';
333
337
  if (!file)
334
338
  return;
335
339
  const url = makeObjectUrl(file);
@@ -342,28 +346,34 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
342
346
  setPendingCropFile(null);
343
347
  setPendingCropClaim(null);
344
348
  if (!blob || !original) {
345
- fail(err("crop_failed", "Cropping failed."));
349
+ fail(err('crop_failed', 'Cropping failed.'));
346
350
  setCropSrc(null);
347
351
  return;
348
352
  }
349
- const type = blob.type || "image/jpeg";
350
- const name = original.name?.replace(/\.[^/.]+$/, "") || "image";
351
- const ext = type.includes("png")
352
- ? "png"
353
- : type.includes("webp")
354
- ? "webp"
355
- : type.includes("avif")
356
- ? "avif"
357
- : "jpg";
353
+ const type = blob.type || 'image/jpeg';
354
+ const name = original.name?.replace(/\.[^/.]+$/, '') || 'image';
355
+ const ext = type.includes('png')
356
+ ? 'png'
357
+ : type.includes('webp')
358
+ ? 'webp'
359
+ : type.includes('avif')
360
+ ? 'avif'
361
+ : 'jpg';
358
362
  const croppedFile = new File([blob], `${name}.cropped.${ext}`, { type });
359
363
  const url = makeObjectUrl(croppedFile);
360
- readMediaMeta(croppedFile).then((meta) => emit({ file: croppedFile, previewUrl: url, meta, claim: preservedClaim ?? undefined, croppedBlob: blob }));
364
+ readMediaMeta(croppedFile).then((meta) => emit({
365
+ file: croppedFile,
366
+ previewUrl: url,
367
+ meta,
368
+ claim: preservedClaim ?? undefined,
369
+ croppedBlob: blob,
370
+ }));
361
371
  setCropSrc(null);
362
372
  }, [emit, fail, pendingCropFile, pendingCropClaim, makeObjectUrl]);
363
373
  const onPhotoCapture = useCallback(async (file) => {
364
374
  setPhotoOpen(false);
365
375
  const url = makeObjectUrl(file);
366
- await handleSelected(file, url, { kind: "image", source: "camera-capture" });
376
+ await handleSelected(file, url, { kind: 'image', source: 'camera-capture' });
367
377
  }, [handleSelected, makeObjectUrl]);
368
378
  const phase = uploadState?.phase ?? null;
369
379
  const percent = uploadState?.percent ?? null;
@@ -386,25 +396,25 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
386
396
  // Warn in dev if no actions are enabled
387
397
  useEffect(() => {
388
398
  if (actionCount === 0) {
389
- console.warn("[MediaInput] No input methods enabled in spec.client. " +
390
- "At least one of allowPick, allowCapturePhoto, allowRecordVideo, or allowRecordAudio must be true.");
399
+ console.warn('[MediaInput] No input methods enabled in spec.client. ' +
400
+ 'At least one of allowPick, allowCapturePhoto, allowRecordVideo, or allowRecordAudio must be true.');
391
401
  }
392
402
  }, [actionCount]);
393
403
  /** Execute an action directly (used for single-action shortcut). */
394
404
  const executeAction = useCallback((action) => {
395
405
  switch (action) {
396
- case "pick":
406
+ case 'pick':
397
407
  pickerRef.current?.click();
398
408
  break;
399
- case "photo":
409
+ case 'photo':
400
410
  setPhotoOpen(true);
401
411
  break;
402
- case "recordVideo":
403
- setRecordKind("video");
412
+ case 'recordVideo':
413
+ setRecordKind('video');
404
414
  setRecordOpen(true);
405
415
  break;
406
- case "recordAudio":
407
- setRecordKind("audio");
416
+ case 'recordAudio':
417
+ setRecordKind('audio');
408
418
  setRecordOpen(true);
409
419
  break;
410
420
  }
@@ -427,27 +437,35 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
427
437
  const actions = useMemo(() => {
428
438
  const list = [];
429
439
  if (canPick)
430
- list.push({ kind: "pick", label: "Choose file", icon: _jsx(Upload, { className: "icon-xs" }) });
440
+ list.push({ kind: 'pick', label: 'Choose file', icon: _jsx(Upload, { className: "icon-xs" }) });
431
441
  if (canPhoto)
432
- list.push({ kind: "photo", label: "Take photo", icon: _jsx(Camera, { className: "icon-xs" }) });
442
+ list.push({ kind: 'photo', label: 'Take photo', icon: _jsx(Camera, { className: "icon-xs" }) });
433
443
  if (canRecVideo)
434
- list.push({ kind: "recordVideo", label: "Record video", icon: _jsx(Video, { className: "icon-xs" }) });
444
+ list.push({
445
+ kind: 'recordVideo',
446
+ label: 'Record video',
447
+ icon: _jsx(Video, { className: "icon-xs" }),
448
+ });
435
449
  if (canRecAudio)
436
- list.push({ kind: "recordAudio", label: "Record audio", icon: _jsx(Mic, { className: "icon-xs" }) });
450
+ list.push({
451
+ kind: 'recordAudio',
452
+ label: 'Record audio',
453
+ icon: _jsx(Mic, { className: "icon-xs" }),
454
+ });
437
455
  return list;
438
456
  }, [canPick, canPhoto, canRecVideo, canRecAudio]);
439
457
  /** Label & icon for the single trigger button. */
440
458
  const triggerIcon = useMemo(() => {
441
459
  if (actions.length === 1)
442
460
  return actions[0].icon;
443
- if (buttonLabel !== "Add media")
461
+ if (buttonLabel !== 'Add media')
444
462
  return _jsx(Upload, { className: "icon-xs" });
445
463
  return _jsx(Plus, { className: "icon-xs" });
446
464
  }, [actions, buttonLabel]);
447
465
  const triggerLabel = useMemo(() => {
448
466
  // If the consumer explicitly set a custom buttonLabel, always respect it.
449
467
  // Only use the action's built-in label for single-action when using the default.
450
- if (buttonLabel !== "Add media")
468
+ if (buttonLabel !== 'Add media')
451
469
  return buttonLabel;
452
470
  if (actions.length === 1)
453
471
  return actions[0].label;
@@ -477,20 +495,20 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
477
495
  setMenuOpen(true);
478
496
  }, [disabled, isLoading, validating, actions, runAction]);
479
497
  useImperativeHandle(ref, () => ({ openSelection }), [openSelection]);
480
- return (_jsxs(Card, { className: cn("p-3", className), children: [_jsx(Input, { id: `media-pick-${id}`, ref: pickerRef, type: "file", accept: acceptAttr, className: "hidden", onChange: onPick, disabled: disabled || isLoading || validating }), selectedFile && !isLoading ? (_jsxs("div", { className: "mb-2 space-y-2", children: [previewKind === "image" || previewKind === "video" || previewKind === "audio" ? (_jsx("div", { className: cn("relative w-full rounded-md bg-muted", previewKind === "audio" ? "p-2" : "h-48 overflow-hidden"), children: _jsx(MediaPreview, { url: selectedFile, type: previewKind, mime: selectedFile.type, controls: true, className: previewKind === "audio" ? "w-full" : "h-full w-full" }) })) : null, _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm truncate flex-1", children: selectedFileLabel }), onClear && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "icon-sm shrink-0", onClick: onClear, "aria-label": clearLabel, children: _jsx(X, { className: "icon-xs" }) }))] })] })) : null, isLoading ? (_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Spinner, { size: "xs" }), _jsx("span", { className: "text-sm text-muted-foreground", children: isPreparing
481
- ? "Preparing..."
498
+ return (_jsxs(Card, { className: cn('p-3', className), children: [_jsx(Input, { id: `media-pick-${id}`, ref: pickerRef, type: "file", accept: acceptAttr, className: "hidden", onChange: onPick, disabled: disabled || isLoading || validating }), selectedFile && !isLoading ? (_jsxs("div", { className: "mb-2 space-y-2", children: [previewKind === 'image' || previewKind === 'video' || previewKind === 'audio' ? (_jsx("div", { className: cn('relative w-full rounded-md bg-muted', previewKind === 'audio' ? 'p-2' : 'h-48 overflow-hidden'), children: _jsx(MediaPreview, { url: selectedFile, type: previewKind, mime: selectedFile.type, controls: true, className: previewKind === 'audio' ? 'w-full' : 'h-full w-full' }) })) : null, _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm truncate flex-1", children: selectedFileLabel }), onClear && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "icon-sm shrink-0", onClick: onClear, "aria-label": clearLabel, children: _jsx(X, { className: "icon-xs" }) }))] })] })) : null, isLoading ? (_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Spinner, { size: "xs" }), _jsx("span", { className: "text-sm text-muted-foreground", children: isPreparing
499
+ ? 'Preparing...'
482
500
  : isUploading && showRealBar
483
501
  ? `Uploading ${Math.round(percent ?? 0)}%...`
484
502
  : isUploading
485
- ? "Uploading..."
503
+ ? 'Uploading...'
486
504
  : isFinalizing
487
- ? "Finalizing..."
488
- : "Finalizing..." }), (isPreparing || isUploading) && onCancel ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "icon-sm shrink-0 ml-auto", onClick: onCancel, "aria-label": "Cancel upload", children: _jsx(X, { className: "icon-xs" }) })) : null] })) : null, showRealBar ? _jsx(Progress, { value: percent ?? 0, className: "mb-2 h-1.5" }) : null, _jsxs("div", { className: "flex flex-wrap gap-2", children: [actions.length <= 1 ? (_jsx(Button, { type: "button", variant: "default", onClick: handleTriggerClick, disabled: disabled || isLoading || actions.length === 0, pending: validating, icon: triggerIcon, children: triggerLabel })) : (_jsxs(DropdownMenu, { open: menuOpen, onOpenChange: setMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "default", disabled: disabled || isLoading, pending: validating, icon: triggerIcon, children: triggerLabel }) }), _jsx(DropdownMenuContent, { align: "start", children: actions.map((a) => (_jsx(DropdownMenuItem, { onClick: () => void runAction(a.kind), icon: a.icon, children: a.label }, a.kind))) })] })), showInfoToggle ? (_jsxs(Button, { type: "button", variant: "outline", onClick: () => setShowInfo((v) => !v), disabled: disabled || isLoading, children: [_jsx(Info, { className: "mr-2 icon-xs" }), showInfo ? "Hide info" : "Info"] })) : null] }), showInfo && showInfoToggle ? (_jsxs("div", { className: "mt-3 space-y-2", children: [infoNotes.length > 0 ? (_jsx("ul", { className: "text-xs text-muted-foreground list-disc pl-4 space-y-1", children: infoNotes.map((n, i) => (_jsx("li", { children: n }, i))) })) : null, specHasConstraints ? _jsx(MediaConstraintsHint, { spec: spec }) : null] })) : null, localError && (_jsx(Alert, { variant: "destructive", className: "mt-3", children: _jsx(AlertDescription, { children: localError.message }) })), cropSpec && (_jsx(ImageCropperModal, { isOpen: cropOpen, onClose: () => {
505
+ ? 'Finalizing...'
506
+ : 'Finalizing...' }), (isPreparing || isUploading) && onCancel ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "icon-sm shrink-0 ml-auto", onClick: onCancel, "aria-label": "Cancel upload", children: _jsx(X, { className: "icon-xs" }) })) : null] })) : null, showRealBar ? _jsx(Progress, { value: percent ?? 0, className: "mb-2 h-1.5" }) : null, _jsxs("div", { className: "flex flex-wrap gap-2", children: [actions.length <= 1 ? (_jsx(Button, { type: "button", variant: "default", onClick: handleTriggerClick, disabled: disabled || isLoading || actions.length === 0, pending: validating, icon: triggerIcon, children: triggerLabel })) : (_jsxs(DropdownMenu, { open: menuOpen, onOpenChange: setMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "default", disabled: disabled || isLoading, pending: validating, icon: triggerIcon, children: triggerLabel }) }), _jsx(DropdownMenuContent, { align: "start", children: actions.map((a) => (_jsx(DropdownMenuItem, { onClick: () => void runAction(a.kind), icon: a.icon, children: a.label }, a.kind))) })] })), showInfoToggle ? (_jsxs(Button, { type: "button", variant: "outline", onClick: () => setShowInfo((v) => !v), disabled: disabled || isLoading, children: [_jsx(Info, { className: "mr-2 icon-xs" }), showInfo ? 'Hide info' : 'Info'] })) : null] }), showInfo && showInfoToggle ? (_jsxs("div", { className: "mt-3 space-y-2", children: [infoNotes.length > 0 ? (_jsx("ul", { className: "text-xs text-muted-foreground list-disc pl-4 space-y-1", children: infoNotes.map((n, i) => (_jsx("li", { children: n }, i))) })) : null, specHasConstraints ? _jsx(MediaConstraintsHint, { spec: spec }) : null] })) : null, localError && (_jsx(Alert, { variant: "destructive", className: "mt-3", children: _jsx(AlertDescription, { children: localError.message }) })), cropSpec && (_jsx(ImageCropperModal, { isOpen: cropOpen, onClose: () => {
489
507
  setCropOpen(false);
490
508
  setCropSrc(null);
491
- }, imageSrc: cropSrc, aspectRatio: cropSpec.aspectRatio, shape: cropSpec.shape ?? "rect", outputWidth: cropSpec.outputWidth, outputHeight: cropSpec.outputHeight, encoding: { format: cropSpec.format, quality: cropSpec.quality }, onCropComplete: onCropComplete })), _jsx(PhotoCaptureModal, { open: photoOpen, facingMode: spec.client?.cameraFacingMode ?? "user", onCapture: onPhotoCapture, onClose: () => setPhotoOpen(false) }), _jsx(RecordDialog, { open: recordOpen, onOpenChange: setRecordOpen, initialKind: recordKind, canPhoto: canPhoto, canRecVideo: canRecVideo, canRecAudio: canRecAudio, cameraFacingMode: spec.client?.cameraFacingMode, maxRecordDurationSec: spec.client?.maxRecordDurationSec, disabled: disabled, isLoading: isLoading, onRecorded: async (file, url, recordedKind) => {
492
- await handleSelected(file, url, { kind: recordedKind, source: "media-recorder" });
509
+ }, imageSrc: cropSrc, aspectRatio: cropSpec.aspectRatio, shape: cropSpec.shape ?? 'rect', outputWidth: cropSpec.outputWidth, outputHeight: cropSpec.outputHeight, encoding: { format: cropSpec.format, quality: cropSpec.quality }, onCropComplete: onCropComplete })), _jsx(PhotoCaptureModal, { open: photoOpen, facingMode: spec.client?.cameraFacingMode ?? 'user', onCapture: onPhotoCapture, onClose: () => setPhotoOpen(false) }), _jsx(RecordDialog, { open: recordOpen, onOpenChange: setRecordOpen, initialKind: recordKind, canPhoto: canPhoto, canRecVideo: canRecVideo, canRecAudio: canRecAudio, cameraFacingMode: spec.client?.cameraFacingMode, maxRecordDurationSec: spec.client?.maxRecordDurationSec, disabled: disabled, isLoading: isLoading, onRecorded: async (file, url, recordedKind) => {
510
+ await handleSelected(file, url, { kind: recordedKind, source: 'media-recorder' });
493
511
  }, onRequestPhoto: () => setPhotoOpen(true) })] }));
494
512
  });
495
- MediaInput.displayName = "MediaInput";
513
+ MediaInput.displayName = 'MediaInput';
496
514
  //# sourceMappingURL=media-input.js.map