@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.
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/image-utils.d.ts +3 -3
- package/dist/lib/image-utils.js +10 -10
- package/dist/lib/image-utils.js.map +1 -1
- package/dist/lib/infer-content-type.d.ts +1 -1
- package/dist/lib/infer-content-type.js +29 -29
- package/dist/lib/infer-content-type.js.map +1 -1
- package/dist/lib/read-media-meta.d.ts +1 -1
- package/dist/lib/read-media-meta.d.ts.map +1 -1
- package/dist/lib/read-media-meta.js +20 -13
- package/dist/lib/read-media-meta.js.map +1 -1
- package/dist/lib/validate-media-duration.js +3 -3
- package/dist/lib/validate-media-duration.js.map +1 -1
- package/dist/lib/validate-upload-kind.d.ts.map +1 -1
- package/dist/lib/validate-upload-kind.js.map +1 -1
- package/dist/progress-bar.d.ts +2 -0
- package/dist/progress-bar.d.ts.map +1 -0
- package/dist/progress-bar.js +2 -0
- package/dist/progress-bar.js.map +1 -0
- package/dist/react/components/audio-input.d.ts +2 -2
- package/dist/react/components/audio-input.js +2 -2
- package/dist/react/components/audio-input.js.map +1 -1
- package/dist/react/components/file-input.d.ts +1 -1
- package/dist/react/components/file-input.d.ts.map +1 -1
- package/dist/react/components/file-input.js +59 -42
- package/dist/react/components/file-input.js.map +1 -1
- package/dist/react/components/image-cropper-modal.d.ts +3 -3
- package/dist/react/components/image-cropper-modal.d.ts.map +1 -1
- package/dist/react/components/image-cropper-modal.js +6 -6
- package/dist/react/components/image-cropper-modal.js.map +1 -1
- package/dist/react/components/image-input.d.ts +2 -2
- package/dist/react/components/image-input.js +2 -2
- package/dist/react/components/image-input.js.map +1 -1
- package/dist/react/components/media-constraints-hint.d.ts +1 -1
- package/dist/react/components/media-constraints-hint.d.ts.map +1 -1
- package/dist/react/components/media-constraints-hint.js +26 -24
- package/dist/react/components/media-constraints-hint.js.map +1 -1
- package/dist/react/components/media-input.d.ts +2 -2
- package/dist/react/components/media-input.d.ts.map +1 -1
- package/dist/react/components/media-input.js +107 -89
- package/dist/react/components/media-input.js.map +1 -1
- package/dist/react/components/photo-capture-modal.d.ts +1 -1
- package/dist/react/components/photo-capture-modal.d.ts.map +1 -1
- package/dist/react/components/photo-capture-modal.js +15 -15
- package/dist/react/components/photo-capture-modal.js.map +1 -1
- package/dist/react/components/record-dialog.d.ts +3 -3
- package/dist/react/components/record-dialog.d.ts.map +1 -1
- package/dist/react/components/record-dialog.js +49 -50
- package/dist/react/components/record-dialog.js.map +1 -1
- package/dist/react/components/video-input.d.ts +2 -2
- package/dist/react/components/video-input.js +2 -2
- package/dist/react/components/video-input.js.map +1 -1
- package/dist/react/index.d.ts +8 -8
- package/dist/react/index.js +8 -8
- package/dist/react/index.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
|
|
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
|
|
4
|
-
import { getSimplifiedMediaType } from
|
|
5
|
-
import { Info, Camera, Mic, Video, Upload, X, Plus } from
|
|
6
|
-
import { Alert, AlertDescription, Button, Card, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Input, Progress, Spinner, } from
|
|
7
|
-
import { cn } from
|
|
8
|
-
import { MediaPreview } from
|
|
9
|
-
import { DEFAULT_PROGRESS_BAR_MIN_BYTES } from
|
|
10
|
-
import { RecordDialog } from
|
|
11
|
-
import { ensureFileWithContentType } from
|
|
12
|
-
import { NEUTRAL_CONTENT_TYPE, projectAcceptTokens } from
|
|
13
|
-
import { ImageCropperModal } from
|
|
14
|
-
import { MediaConstraintsHint } from
|
|
15
|
-
import { PhotoCaptureModal } from
|
|
16
|
-
import { readMediaMeta } from
|
|
17
|
-
import { validateMediaDuration } from
|
|
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 !==
|
|
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 ===
|
|
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 ===
|
|
85
|
+
if (!required || required === 'any')
|
|
84
86
|
return true;
|
|
85
|
-
if (!actual || actual ===
|
|
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 =
|
|
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(
|
|
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(
|
|
183
|
-
return
|
|
184
|
-
if (k.includes(
|
|
185
|
-
return
|
|
186
|
-
if (k.includes(
|
|
187
|
-
return
|
|
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 ===
|
|
194
|
-
?
|
|
195
|
-
: previewKind ===
|
|
196
|
-
?
|
|
197
|
-
: previewKind ===
|
|
198
|
-
?
|
|
199
|
-
:
|
|
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 ===
|
|
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 ===
|
|
220
|
-
? action.kind ===
|
|
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(
|
|
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:
|
|
237
|
-
kind: meta.kind ===
|
|
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(
|
|
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 ===
|
|
255
|
+
if ((meta.kind === 'video' || meta.kind === 'audio') && maxDur) {
|
|
252
256
|
const ok = await validateMediaDuration(safeFile, maxDur);
|
|
253
257
|
if (!ok) {
|
|
254
|
-
fail(err(
|
|
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 ===
|
|
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(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
349
|
+
fail(err('crop_failed', 'Cropping failed.'));
|
|
346
350
|
setCropSrc(null);
|
|
347
351
|
return;
|
|
348
352
|
}
|
|
349
|
-
const type = blob.type ||
|
|
350
|
-
const name = original.name?.replace(/\.[^/.]+$/,
|
|
351
|
-
const ext = type.includes(
|
|
352
|
-
?
|
|
353
|
-
: type.includes(
|
|
354
|
-
?
|
|
355
|
-
: type.includes(
|
|
356
|
-
?
|
|
357
|
-
:
|
|
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({
|
|
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:
|
|
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(
|
|
390
|
-
|
|
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
|
|
406
|
+
case 'pick':
|
|
397
407
|
pickerRef.current?.click();
|
|
398
408
|
break;
|
|
399
|
-
case
|
|
409
|
+
case 'photo':
|
|
400
410
|
setPhotoOpen(true);
|
|
401
411
|
break;
|
|
402
|
-
case
|
|
403
|
-
setRecordKind(
|
|
412
|
+
case 'recordVideo':
|
|
413
|
+
setRecordKind('video');
|
|
404
414
|
setRecordOpen(true);
|
|
405
415
|
break;
|
|
406
|
-
case
|
|
407
|
-
setRecordKind(
|
|
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:
|
|
440
|
+
list.push({ kind: 'pick', label: 'Choose file', icon: _jsx(Upload, { className: "icon-xs" }) });
|
|
431
441
|
if (canPhoto)
|
|
432
|
-
list.push({ kind:
|
|
442
|
+
list.push({ kind: 'photo', label: 'Take photo', icon: _jsx(Camera, { className: "icon-xs" }) });
|
|
433
443
|
if (canRecVideo)
|
|
434
|
-
list.push({
|
|
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({
|
|
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 !==
|
|
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 !==
|
|
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(
|
|
481
|
-
?
|
|
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
|
-
?
|
|
503
|
+
? 'Uploading...'
|
|
486
504
|
: isFinalizing
|
|
487
|
-
?
|
|
488
|
-
:
|
|
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 ??
|
|
492
|
-
await handleSelected(file, url, { kind: recordedKind, source:
|
|
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 =
|
|
513
|
+
MediaInput.displayName = 'MediaInput';
|
|
496
514
|
//# sourceMappingURL=media-input.js.map
|