@ttt-productions/file-input 0.11.9 → 0.11.11
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/lib/infer-content-type.d.ts +0 -2
- package/dist/lib/infer-content-type.d.ts.map +1 -1
- package/dist/lib/infer-content-type.js +2 -3
- package/dist/lib/infer-content-type.js.map +1 -1
- package/dist/react/components/file-input.js +3 -3
- package/dist/react/components/file-input.js.map +1 -1
- package/dist/react/components/image-cropper-modal.d.ts.map +1 -1
- package/dist/react/components/image-cropper-modal.js +9 -12
- package/dist/react/components/image-cropper-modal.js.map +1 -1
- package/dist/react/components/media-input.d.ts.map +1 -1
- package/dist/react/components/media-input.js +37 -18
- package/dist/react/components/media-input.js.map +1 -1
- package/dist/react/components/photo-capture-modal.d.ts.map +1 -1
- package/dist/react/components/photo-capture-modal.js +23 -17
- package/dist/react/components/photo-capture-modal.js.map +1 -1
- package/dist/react/components/record-dialog.d.ts.map +1 -1
- package/dist/react/components/record-dialog.js +33 -6
- package/dist/react/components/record-dialog.js.map +1 -1
- package/package.json +6 -5
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
export type InferKind = "image" | "video" | "audio";
|
|
2
|
-
/** The neutral pass-through type for unknown picker metadata. */
|
|
3
|
-
export declare const NEUTRAL_CONTENT_TYPE = "application/octet-stream";
|
|
4
2
|
/**
|
|
5
3
|
* Returns a valid, parameter-less media MIME for the given File.
|
|
6
4
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"infer-content-type.d.ts","sourceRoot":"","sources":["../../src/lib/infer-content-type.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"infer-content-type.d.ts","sourceRoot":"","sources":["../../src/lib/infer-content-type.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAoEpD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,EAAE,SAAS,GAAG,MAAM,CAgB7E;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,EAAE,SAAS,GAAG,IAAI,CAIpF"}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// behavior defaulted such files to `image/jpeg`, which invented a false media
|
|
17
17
|
// fact the whole pipeline then trusted. upload-core accepts the neutral type
|
|
18
18
|
// only via its explicit opt-in (`allowNeutralContentType`).
|
|
19
|
+
import { NEUTRAL_CONTENT_TYPE } from "@ttt-productions/media-schemas";
|
|
19
20
|
const EXT_TO_MIME = {
|
|
20
21
|
// image
|
|
21
22
|
jpg: "image/jpeg",
|
|
@@ -55,8 +56,6 @@ const KIND_DEFAULT = {
|
|
|
55
56
|
audio: "audio/webm",
|
|
56
57
|
};
|
|
57
58
|
const BASE_MEDIA_MIME_RE = /^(image|video|audio)\/[a-z0-9.+-]+$/;
|
|
58
|
-
/** The neutral pass-through type for unknown picker metadata. */
|
|
59
|
-
export const NEUTRAL_CONTENT_TYPE = "application/octet-stream";
|
|
60
59
|
/**
|
|
61
60
|
* Extracts the parameter-less base MIME ("audio/webm" from
|
|
62
61
|
* "audio/webm;codecs=opus"), lowercased, when it is a usable media type.
|
|
@@ -67,7 +66,7 @@ function parseBaseMime(mime) {
|
|
|
67
66
|
if (!mime)
|
|
68
67
|
return null;
|
|
69
68
|
const base = mime.split(";")[0].trim().toLowerCase();
|
|
70
|
-
if (base ===
|
|
69
|
+
if (base === NEUTRAL_CONTENT_TYPE)
|
|
71
70
|
return null;
|
|
72
71
|
return BASE_MEDIA_MIME_RE.test(base) ? base : null;
|
|
73
72
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"infer-content-type.js","sourceRoot":"","sources":["../../src/lib/infer-content-type.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,+EAA+E;AAC/E,2EAA2E;AAC3E,wDAAwD;AACxD,EAAE;AACF,sEAAsE;AACtE,gEAAgE;AAChE,uEAAuE;AACvE,gCAAgC;AAChC,EAAE;AACF,uEAAuE;AACvE,+EAA+E;AAC/E,iEAAiE;AACjE,4EAA4E;AAC5E,0EAA0E;AAC1E,8EAA8E;AAC9E,6EAA6E;AAC7E,4DAA4D;
|
|
1
|
+
{"version":3,"file":"infer-content-type.js","sourceRoot":"","sources":["../../src/lib/infer-content-type.ts"],"names":[],"mappings":"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","sourcesContent":["// Defense-in-depth: guarantee every File we emit has a NORMALIZED, honest\n// content type. If file.type is missing or unusable, infer from the extension;\n// with a caller-declared kind (recorder/capture — the caller KNOWS what it\n// produced) fall back to that kind's default container.\n//\n// MediaRecorder (and some pickers) report parameterized types such as\n// \"audio/webm;codecs=opus\". Downstream storage rules validate a\n// parameter-less base type, so we always emit the base (\"audio/webm\"),\n// never the parameterized form.\n//\n// UNKNOWN PICKER METADATA IS NOT FABRICATED (canonical-upload-content-\n// classification, 2026-07-31): when there is no declared kind, no usable MIME,\n// and no known extension, the file passes through as the NEUTRAL\n// `application/octet-stream` so the server's canonical byte inspector — the\n// only classification authority — sees \"unknown\" as unknown. The previous\n// behavior defaulted such files to `image/jpeg`, which invented a false media\n// fact the whole pipeline then trusted. upload-core accepts the neutral type\n// only via its explicit opt-in (`allowNeutralContentType`).\n\nimport { NEUTRAL_CONTENT_TYPE } from \"@ttt-productions/media-schemas\";\n\nexport type InferKind = \"image\" | \"video\" | \"audio\";\n\nconst EXT_TO_MIME: Record<string, string> = {\n // image\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n avif: \"image/avif\",\n heic: \"image/heic\",\n heif: \"image/heif\",\n // video\n mp4: \"video/mp4\",\n m4v: \"video/mp4\",\n mov: \"video/quicktime\",\n webm: \"video/webm\",\n mkv: \"video/x-matroska\",\n // audio\n mp3: \"audio/mpeg\",\n m4a: \"audio/mp4\",\n aac: \"audio/aac\",\n wav: \"audio/wav\",\n ogg: \"audio/ogg\",\n oga: \"audio/ogg\",\n flac: \"audio/flac\",\n};\n\n// Containers that legitimately hold either audio or video. When the caller\n// declares a kind (e.g. the record dialog's chosen recording kind), that kind\n// picks the matching side; without a kind we keep the historical single-kind\n// guess in EXT_TO_MIME (webm → video, ogg → audio).\nconst AMBIGUOUS_EXT_TO_MIME: Record<string, Partial<Record<InferKind, string>>> = {\n webm: { audio: \"audio/webm\", video: \"video/webm\" },\n ogg: { audio: \"audio/ogg\", video: \"video/ogg\" },\n};\n\nconst KIND_DEFAULT: Record<InferKind, string> = {\n image: \"image/jpeg\",\n video: \"video/webm\",\n audio: \"audio/webm\",\n};\n\nconst BASE_MEDIA_MIME_RE = /^(image|video|audio)\\/[a-z0-9.+-]+$/;\n\n/**\n * Extracts the parameter-less base MIME (\"audio/webm\" from\n * \"audio/webm;codecs=opus\"), lowercased, when it is a usable media type.\n * Returns null for empty values, application/octet-stream, and anything\n * whose base is not image/*, video/*, or audio/*.\n */\nfunction parseBaseMime(mime: string | undefined | null): string | null {\n if (!mime) return null;\n const base = mime.split(\";\")[0].trim().toLowerCase();\n if (base === NEUTRAL_CONTENT_TYPE) return null;\n return BASE_MEDIA_MIME_RE.test(base) ? base : null;\n}\n\nfunction kindOfMime(base: string): InferKind {\n return base.slice(0, base.indexOf(\"/\")) as InferKind;\n}\n\nfunction extOf(name: string): string | null {\n const dot = name.lastIndexOf(\".\");\n if (dot < 0 || dot === name.length - 1) return null;\n return name.slice(dot + 1).toLowerCase();\n}\n\n/**\n * Returns a valid, parameter-less media MIME for the given File.\n *\n * Precedence:\n * 1. The base of file.type (parameters stripped, lowercased) when valid —\n * unless `fallbackKind` is supplied and disagrees with the declared\n * top-level kind. A caller-declared kind (e.g. the record dialog's chosen\n * recording kind) is authoritative context, so a conflicting declared type\n * is treated as untrustworthy and resolution falls through to the\n * extension.\n * 2. Extension lookup. For ambiguous containers (webm/ogg exist as both audio\n * and video) a supplied `fallbackKind` picks the matching side; unambiguous\n * extensions keep their single mapping.\n * 3. The kind default for `fallbackKind`; without one, the NEUTRAL type — an\n * unknown picker file is never rewritten into a false media fact.\n */\nexport function inferContentType(file: File, fallbackKind?: InferKind): string {\n const base = parseBaseMime(file.type);\n if (base && (!fallbackKind || kindOfMime(base) === fallbackKind)) return base;\n\n const ext = extOf(file.name);\n if (ext) {\n const byKind = fallbackKind ? AMBIGUOUS_EXT_TO_MIME[ext]?.[fallbackKind] : undefined;\n if (byKind) return byKind;\n if (EXT_TO_MIME[ext]) return EXT_TO_MIME[ext];\n }\n\n if (fallbackKind) return KIND_DEFAULT[fallbackKind];\n\n // No declared kind, no usable MIME, no known extension: NEUTRAL, never a\n // fabricated media type. The server inspector owns classification.\n return NEUTRAL_CONTENT_TYPE;\n}\n\n/**\n * Returns a File whose .type is exactly the valid, parameter-less media MIME\n * from inferContentType. If the original file already carries that exact\n * type, it is returned unchanged; otherwise (missing type, parameterized\n * type, wrong case, or a type overridden by fallbackKind precedence) the\n * bytes are wrapped in a new File with the normalized type.\n */\nexport function ensureFileWithContentType(file: File, fallbackKind?: InferKind): File {\n const type = inferContentType(file, fallbackKind);\n if (file.type === type) return file;\n return new File([file], file.name, { type, lastModified: file.lastModified });\n}\n"]}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState, useId } from "react";
|
|
4
4
|
import { getSimplifiedMediaType } from "@ttt-productions/media-schemas";
|
|
5
|
-
import { Alert, AlertDescription, Button, Card, Input, Progress } from "@ttt-productions/ui-core/react";
|
|
5
|
+
import { Alert, AlertDescription, Button, Card, Input, Progress, Spinner } from "@ttt-productions/ui-core/react";
|
|
6
6
|
import { cn } from "@ttt-productions/ui-core";
|
|
7
|
-
import { Info, X, Upload, Film, Music, Paperclip, AlertTriangle
|
|
7
|
+
import { Info, X, Upload, Film, Music, Paperclip, AlertTriangle } from "lucide-react";
|
|
8
8
|
import { validateMediaDuration } from "../../lib/validate-media-duration.js";
|
|
9
9
|
import { ImageCropperModal } from "./image-cropper-modal.js";
|
|
10
10
|
const MIME_MAP = {
|
|
@@ -167,7 +167,7 @@ export function FileInput(props) {
|
|
|
167
167
|
});
|
|
168
168
|
};
|
|
169
169
|
const isUploading = typeof uploadProgress === "number" && isLoading;
|
|
170
|
-
return (_jsxs("div", { className: cn("relative w-full", className), children: [localError && (_jsxs(Alert, { variant: "destructive", className: "mb-2", children: [_jsx(AlertTriangle, { className: "icon-xs" }), _jsx(AlertDescription, { children: localError.message })] })), _jsx(Input, { id: inputId, type: "file", accept: acceptString, onChange: handleFileChange, className: "hidden", disabled: disabled || isLoading, ref: fileInputRef }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { htmlFor: inputId, className: "w-full flex-grow", children: _jsx(Button, { asChild: true, disabled: disabled || isLoading, variant: variant, size: size, className: cn("w-full relative overflow-hidden cursor-pointer", { "icon-xl p-0": size === "icon" }), children: _jsxs("span", { className: "relative w-full h-full center-row", children: [isUploading && (_jsx(Progress, { value: uploadProgress ?? 0, className: "absolute left-0 top-0 h-full w-full z-0 bg-accent" })), _jsx("span", { className: "z-10 center-row w-full", children: isLoading ? (_jsxs(_Fragment, { children: [_jsx(
|
|
170
|
+
return (_jsxs("div", { className: cn("relative w-full", className), children: [localError && (_jsxs(Alert, { variant: "destructive", className: "mb-2", children: [_jsx(AlertTriangle, { className: "icon-xs" }), _jsx(AlertDescription, { children: localError.message })] })), _jsx(Input, { id: inputId, type: "file", accept: acceptString, onChange: handleFileChange, className: "hidden", disabled: disabled || isLoading, ref: fileInputRef }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { htmlFor: inputId, className: "w-full flex-grow", children: _jsx(Button, { asChild: true, disabled: disabled || isLoading, variant: variant, size: size, className: cn("w-full relative overflow-hidden cursor-pointer", { "icon-xl p-0": size === "icon" }), children: _jsxs("span", { className: "relative w-full h-full center-row", children: [isUploading && (_jsx(Progress, { value: uploadProgress ?? 0, className: "absolute left-0 top-0 h-full w-full z-0 bg-accent" })), _jsx("span", { className: "z-10 center-row w-full", children: isLoading ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { size: "xs", className: "mr-2" }), " ", isUploading ? `${(uploadProgress ?? 0).toFixed(0)}%` : "Processing..."] })) : fileValue ? (_jsxs("span", { className: "flex items-center justify-between w-full", children: [_jsx("span", { className: "truncate pr-2", children: getSelectedFileLabel(fileValue) }), _jsx(Button, { variant: "ghost", size: "icon", className: "icon-sm hover:bg-destructive/20 shrink-0", onClick: handleClear, children: _jsx(X, { className: "icon-xs" }) })] })) : (_jsx(_Fragment, { children: buttonLabel === "Attach"
|
|
171
171
|
? _jsxs(_Fragment, { children: [_jsx(Paperclip, { className: "mr-2 icon-xs" }), " ", buttonLabel] })
|
|
172
172
|
: _jsxs(_Fragment, { children: [getUploadIcon(), " ", buttonLabel] }) })) })] }) }) }), detailsDescriptionParts.length > 0 && size !== "icon" && (_jsx(Button, { type: "button", variant: "default", size: "icon", onClick: () => setShowDetails((p) => !p), "aria-label": "Show file processing details", className: "flex-shrink-0 icon-xl", children: _jsx(Info, { className: "icon-sm" }) }))] }), showDetails && detailsDescriptionParts.length > 0 && size !== "icon" && (_jsx(Card, { className: "page-card w-full mt-2", children: _jsx("div", { className: "text-center p-2 space-y-0.5", children: detailsDescriptionParts.map((part, i) => (_jsx("p", { className: "text-small font-bold", children: part }, i))) }) })), cropConfig && imageToCrop && (_jsx(ImageCropperModal, { isOpen: isCropperOpen, onClose: () => {
|
|
173
173
|
setIsCropperOpen(false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-input.js","sourceRoot":"","sources":["../../../src/react/components/file-input.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState, useId } from \"react\";\nimport { getSimplifiedMediaType, type SimplifiedMediaType } from \"@ttt-productions/media-schemas\";\nimport { Alert, AlertDescription, Button, Card, Input, Progress } from \"@ttt-productions/ui-core/react\";\nimport { cn } from \"@ttt-productions/ui-core\";\nimport { Info, X, Upload, Film, Music, Paperclip, AlertTriangle, Loader2 } from \"lucide-react\";\n\nimport type { FileInputError, FileInputProps } from \"../../types.js\";\nimport { validateMediaDuration } from \"../../lib/validate-media-duration.js\";\nimport { ImageCropperModal } from \"./image-cropper-modal.js\";\n\nconst MIME_MAP: Record<SimplifiedMediaType, string> = {\n image: \"image/jpeg, image/png, image/gif, image/webp, image/svg+xml, image/bmp, image/avif\",\n video: \"video/*, .mkv\",\n audio: \"audio/*\",\n other: \".pdf, .doc, .docx\"\n};\n\nfunction err(code: FileInputError[\"code\"], message: string, details?: Record<string, unknown>): FileInputError {\n return { code, message, details };\n}\n\nfunction getSelectedFileLabel(file: File): string {\n switch (getSimplifiedMediaType(file)) {\n case \"image\": return \"Image selected\";\n case \"video\": return \"Video selected\";\n case \"audio\": return \"Audio selected\";\n default: return \"File selected\";\n }\n}\n\nexport function FileInput(props: FileInputProps) {\n const {\n acceptTypes,\n maxSizeMB,\n selectedFile,\n onChange,\n onError,\n isLoading = false,\n disabled = false,\n buttonLabel = \"Upload File\",\n className,\n uploadProgress = null,\n variant = \"default\",\n size = \"default\",\n cropConfig,\n videoMaxDurationSec,\n audioMaxDurationSec,\n defaultShowDetails = false,\n } = props;\n\n const fileInputRef = useRef<HTMLInputElement>(null);\n const lastPreviewUrlRef = useRef<string | null>(null);\n\n const revokeLastPreviewUrl = useCallback(() => {\n if (!lastPreviewUrlRef.current) return;\n try {\n URL.revokeObjectURL(lastPreviewUrlRef.current);\n } catch {}\n lastPreviewUrlRef.current = null;\n }, []);\n\n const makePreviewUrl = useCallback((blob: Blob) => {\n revokeLastPreviewUrl();\n const url = URL.createObjectURL(blob);\n lastPreviewUrlRef.current = url;\n return url;\n }, [revokeLastPreviewUrl]);\n\n useEffect(() => {\n return () => revokeLastPreviewUrl();\n }, [revokeLastPreviewUrl]);\n const uniqueId = useId();\n const inputId = `file-input-${uniqueId}`;\n\n const [internalSelected, setInternalSelected] = useState<File | null>(null);\n const fileValue = selectedFile !== undefined ? selectedFile : internalSelected;\n\n const [localError, setLocalError] = useState<FileInputError | null>(null);\n\n const [isCropperOpen, setIsCropperOpen] = useState(false);\n const [imageToCrop, setImageToCrop] = useState<string | null>(null);\n\n const [showDetails, setShowDetails] = useState(defaultShowDetails);\n\n const acceptString = useMemo(\n () => (acceptTypes.length ? acceptTypes.map((t) => MIME_MAP[t]).join(\", \") : undefined),\n [acceptTypes]\n );\n\n const detailsDescriptionParts = useMemo(() => {\n const parts: string[] = [];\n\n if (cropConfig) {\n const ratioDisplay = cropConfig.aspectRatioDisplay || `${cropConfig.aspectRatio.toFixed(2)}:1`;\n parts.push(`Image will be cropped to a ${ratioDisplay} rect (${cropConfig.outputWidth}x${cropConfig.outputHeight}px).`);\n }\n\n const sizeParts = acceptTypes\n .map((t) => (maxSizeMB[t] ? `${t.charAt(0).toUpperCase() + t.slice(1)} (${maxSizeMB[t]}MB)` : null))\n .filter(Boolean) as string[];\n\n if (sizeParts.length) parts.push(`Max sizes: ${sizeParts.join(\", \")}.`);\n\n if (videoMaxDurationSec) parts.push(`Max video duration: ${videoMaxDurationSec}s.`);\n if (audioMaxDurationSec) parts.push(`Max audio duration: ${audioMaxDurationSec}s.`);\n\n return parts;\n }, [acceptTypes, maxSizeMB, cropConfig, videoMaxDurationSec, audioMaxDurationSec]);\n\n const getUploadIcon = () => {\n if (acceptTypes.includes(\"image\")) return <Upload className=\"mr-2 icon-xs\" />;\n if (acceptTypes.includes(\"video\")) return <Film className=\"mr-2 icon-xs\" />;\n if (acceptTypes.includes(\"audio\")) return <Music className=\"mr-2 icon-xs\" />;\n return <Upload className=\"mr-2 icon-xs\" />;\n };\n\n const clearNative = () => {\n if (fileInputRef.current) fileInputRef.current.value = \"\";\n };\n\n const emitError = useCallback((e: FileInputError, type: SimplifiedMediaType = \"other\") => {\n setLocalError(e);\n onError?.(e);\n onChange({ type, error: e });\n }, [onError, onChange]);\n\n const handleClear = (e: React.MouseEvent<HTMLButtonElement>) => {\n e.stopPropagation();\n e.preventDefault();\n setLocalError(null);\n setInternalSelected(null);\n clearNative();\n onChange({ type: \"other\" });\n };\n\n const handleFileChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n clearNative();\n setLocalError(null);\n\n if (!file) {\n setInternalSelected(null);\n onChange({ type: \"other\" });\n return;\n }\n\n const fileType = getSimplifiedMediaType(file);\n if (acceptTypes.length && !acceptTypes.includes(fileType)) {\n emitError(err(\"invalid_type\", `Invalid file type. Allowed: ${acceptTypes.join(\", \")}.`), fileType);\n return;\n }\n\n const maxBytes = (maxSizeMB[fileType] ?? Infinity) * 1024 * 1024;\n if (file.size > maxBytes) {\n emitError(err(\"too_large\", `File too large. Max ${maxSizeMB[fileType]}MB for ${fileType}.`, { size: file.size, maxBytes }), fileType);\n return;\n }\n\n if (fileType === \"video\" && videoMaxDurationSec) {\n const ok = await validateMediaDuration(file, videoMaxDurationSec);\n if (!ok) {\n emitError(err(\"too_long\", `Video too long. Max ${videoMaxDurationSec} seconds.`), fileType);\n return;\n }\n }\n\n if (fileType === \"audio\" && audioMaxDurationSec) {\n const ok = await validateMediaDuration(file, audioMaxDurationSec);\n if (!ok) {\n emitError(err(\"too_long\", `Audio too long. Max ${audioMaxDurationSec} seconds.`), fileType);\n return;\n }\n }\n\n // cropping path\n if (cropConfig && fileType === \"image\") {\n const reader = new FileReader();\n reader.onload = () => {\n setImageToCrop(reader.result as string);\n setIsCropperOpen(true);\n };\n reader.onerror = () => {\n emitError(err(\"read_failed\", \"Failed to read image for cropping.\"));\n };\n reader.readAsDataURL(file);\n setInternalSelected(file);\n return;\n }\n\n // normal selection\n setInternalSelected(file);\n const previewUrl = makePreviewUrl(file);\n onChange({ type: fileType, file, previewUrl, revokePreviewUrl: revokeLastPreviewUrl });\n }, [acceptTypes, maxSizeMB, cropConfig, videoMaxDurationSec, audioMaxDurationSec, emitError, makePreviewUrl, onChange, revokeLastPreviewUrl]);\n\n const handleCropComplete = (blob: Blob | null) => {\n setIsCropperOpen(false);\n\n const original = internalSelected;\n setImageToCrop(null);\n\n if (!blob) {\n emitError(err(\"crop_failed\", \"Cropping failed.\"), \"image\");\n return;\n }\n\n onChange({\n type: \"image\",\n file: original ?? undefined,\n blob,\n previewUrl: makePreviewUrl(blob),\n revokePreviewUrl: revokeLastPreviewUrl,\n });\n };\n\n const isUploading = typeof uploadProgress === \"number\" && isLoading;\n\n return (\n <div className={cn(\"relative w-full\", className)}>\n {localError && (\n <Alert variant=\"destructive\" className=\"mb-2\">\n <AlertTriangle className=\"icon-xs\" />\n <AlertDescription>{localError.message}</AlertDescription>\n </Alert>\n )}\n\n <Input\n id={inputId}\n type=\"file\"\n accept={acceptString}\n onChange={handleFileChange}\n className=\"hidden\"\n disabled={disabled || isLoading}\n ref={fileInputRef}\n />\n\n <div className=\"flex items-center gap-2\">\n <label htmlFor={inputId} className=\"w-full flex-grow\">\n <Button\n asChild\n disabled={disabled || isLoading}\n variant={variant}\n size={size}\n className={cn(\"w-full relative overflow-hidden cursor-pointer\", { \"icon-xl p-0\": size === \"icon\" })}\n >\n <span className=\"relative w-full h-full center-row\">\n {isUploading && (\n <Progress value={uploadProgress ?? 0} className=\"absolute left-0 top-0 h-full w-full z-0 bg-accent\" />\n )}\n\n <span className=\"z-10 center-row w-full\">\n {isLoading ? (\n <><Loader2 className=\"mr-2 spinner-xs\" /> {isUploading ? `${(uploadProgress ?? 0).toFixed(0)}%` : \"Processing...\"}</>\n ) : fileValue ? (\n <span className=\"flex items-center justify-between w-full\">\n <span className=\"truncate pr-2\">{getSelectedFileLabel(fileValue)}</span>\n <Button variant=\"ghost\" size=\"icon\" className=\"icon-sm hover:bg-destructive/20 shrink-0\" onClick={handleClear}>\n <X className=\"icon-xs\" />\n </Button>\n </span>\n ) : (\n <>\n {buttonLabel === \"Attach\"\n ? <><Paperclip className=\"mr-2 icon-xs\" /> {buttonLabel}</>\n : <>{getUploadIcon()} {buttonLabel}</>}\n </>\n )}\n </span>\n </span>\n </Button>\n </label>\n\n {detailsDescriptionParts.length > 0 && size !== \"icon\" && (\n <Button\n type=\"button\"\n variant=\"default\"\n size=\"icon\"\n onClick={() => setShowDetails((p) => !p)}\n aria-label=\"Show file processing details\"\n className=\"flex-shrink-0 icon-xl\"\n >\n <Info className=\"icon-sm\" />\n </Button>\n )}\n </div>\n\n {showDetails && detailsDescriptionParts.length > 0 && size !== \"icon\" && (\n <Card className=\"page-card w-full mt-2\">\n <div className=\"text-center p-2 space-y-0.5\">\n {detailsDescriptionParts.map((part, i) => (\n <p key={i} className=\"text-small font-bold\">{part}</p>\n ))}\n </div>\n </Card>\n )}\n\n {cropConfig && imageToCrop && (\n <ImageCropperModal\n isOpen={isCropperOpen}\n onClose={() => {\n setIsCropperOpen(false);\n setImageToCrop(null);\n }}\n imageSrc={imageToCrop}\n aspectRatio={cropConfig.aspectRatio}\n shape={cropConfig.shape}\n outputWidth={cropConfig.outputWidth}\n outputHeight={cropConfig.outputHeight}\n onCropComplete={handleCropComplete}\n />\n )}\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"file-input.js","sourceRoot":"","sources":["../../../src/react/components/file-input.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState, useId } from \"react\";\nimport { getSimplifiedMediaType, type SimplifiedMediaType } from \"@ttt-productions/media-schemas\";\nimport { Alert, AlertDescription, Button, Card, Input, Progress, Spinner } from \"@ttt-productions/ui-core/react\";\nimport { cn } from \"@ttt-productions/ui-core\";\nimport { Info, X, Upload, Film, Music, Paperclip, AlertTriangle } from \"lucide-react\";\n\nimport type { FileInputError, FileInputProps } from \"../../types.js\";\nimport { validateMediaDuration } from \"../../lib/validate-media-duration.js\";\nimport { ImageCropperModal } from \"./image-cropper-modal.js\";\n\nconst MIME_MAP: Record<SimplifiedMediaType, string> = {\n image: \"image/jpeg, image/png, image/gif, image/webp, image/svg+xml, image/bmp, image/avif\",\n video: \"video/*, .mkv\",\n audio: \"audio/*\",\n other: \".pdf, .doc, .docx\"\n};\n\nfunction err(code: FileInputError[\"code\"], message: string, details?: Record<string, unknown>): FileInputError {\n return { code, message, details };\n}\n\nfunction getSelectedFileLabel(file: File): string {\n switch (getSimplifiedMediaType(file)) {\n case \"image\": return \"Image selected\";\n case \"video\": return \"Video selected\";\n case \"audio\": return \"Audio selected\";\n default: return \"File selected\";\n }\n}\n\nexport function FileInput(props: FileInputProps) {\n const {\n acceptTypes,\n maxSizeMB,\n selectedFile,\n onChange,\n onError,\n isLoading = false,\n disabled = false,\n buttonLabel = \"Upload File\",\n className,\n uploadProgress = null,\n variant = \"default\",\n size = \"default\",\n cropConfig,\n videoMaxDurationSec,\n audioMaxDurationSec,\n defaultShowDetails = false,\n } = props;\n\n const fileInputRef = useRef<HTMLInputElement>(null);\n const lastPreviewUrlRef = useRef<string | null>(null);\n\n const revokeLastPreviewUrl = useCallback(() => {\n if (!lastPreviewUrlRef.current) return;\n try {\n URL.revokeObjectURL(lastPreviewUrlRef.current);\n } catch {}\n lastPreviewUrlRef.current = null;\n }, []);\n\n const makePreviewUrl = useCallback((blob: Blob) => {\n revokeLastPreviewUrl();\n const url = URL.createObjectURL(blob);\n lastPreviewUrlRef.current = url;\n return url;\n }, [revokeLastPreviewUrl]);\n\n useEffect(() => {\n return () => revokeLastPreviewUrl();\n }, [revokeLastPreviewUrl]);\n const uniqueId = useId();\n const inputId = `file-input-${uniqueId}`;\n\n const [internalSelected, setInternalSelected] = useState<File | null>(null);\n const fileValue = selectedFile !== undefined ? selectedFile : internalSelected;\n\n const [localError, setLocalError] = useState<FileInputError | null>(null);\n\n const [isCropperOpen, setIsCropperOpen] = useState(false);\n const [imageToCrop, setImageToCrop] = useState<string | null>(null);\n\n const [showDetails, setShowDetails] = useState(defaultShowDetails);\n\n const acceptString = useMemo(\n () => (acceptTypes.length ? acceptTypes.map((t) => MIME_MAP[t]).join(\", \") : undefined),\n [acceptTypes]\n );\n\n const detailsDescriptionParts = useMemo(() => {\n const parts: string[] = [];\n\n if (cropConfig) {\n const ratioDisplay = cropConfig.aspectRatioDisplay || `${cropConfig.aspectRatio.toFixed(2)}:1`;\n parts.push(`Image will be cropped to a ${ratioDisplay} rect (${cropConfig.outputWidth}x${cropConfig.outputHeight}px).`);\n }\n\n const sizeParts = acceptTypes\n .map((t) => (maxSizeMB[t] ? `${t.charAt(0).toUpperCase() + t.slice(1)} (${maxSizeMB[t]}MB)` : null))\n .filter(Boolean) as string[];\n\n if (sizeParts.length) parts.push(`Max sizes: ${sizeParts.join(\", \")}.`);\n\n if (videoMaxDurationSec) parts.push(`Max video duration: ${videoMaxDurationSec}s.`);\n if (audioMaxDurationSec) parts.push(`Max audio duration: ${audioMaxDurationSec}s.`);\n\n return parts;\n }, [acceptTypes, maxSizeMB, cropConfig, videoMaxDurationSec, audioMaxDurationSec]);\n\n const getUploadIcon = () => {\n if (acceptTypes.includes(\"image\")) return <Upload className=\"mr-2 icon-xs\" />;\n if (acceptTypes.includes(\"video\")) return <Film className=\"mr-2 icon-xs\" />;\n if (acceptTypes.includes(\"audio\")) return <Music className=\"mr-2 icon-xs\" />;\n return <Upload className=\"mr-2 icon-xs\" />;\n };\n\n const clearNative = () => {\n if (fileInputRef.current) fileInputRef.current.value = \"\";\n };\n\n const emitError = useCallback((e: FileInputError, type: SimplifiedMediaType = \"other\") => {\n setLocalError(e);\n onError?.(e);\n onChange({ type, error: e });\n }, [onError, onChange]);\n\n const handleClear = (e: React.MouseEvent<HTMLButtonElement>) => {\n e.stopPropagation();\n e.preventDefault();\n setLocalError(null);\n setInternalSelected(null);\n clearNative();\n onChange({ type: \"other\" });\n };\n\n const handleFileChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n clearNative();\n setLocalError(null);\n\n if (!file) {\n setInternalSelected(null);\n onChange({ type: \"other\" });\n return;\n }\n\n const fileType = getSimplifiedMediaType(file);\n if (acceptTypes.length && !acceptTypes.includes(fileType)) {\n emitError(err(\"invalid_type\", `Invalid file type. Allowed: ${acceptTypes.join(\", \")}.`), fileType);\n return;\n }\n\n const maxBytes = (maxSizeMB[fileType] ?? Infinity) * 1024 * 1024;\n if (file.size > maxBytes) {\n emitError(err(\"too_large\", `File too large. Max ${maxSizeMB[fileType]}MB for ${fileType}.`, { size: file.size, maxBytes }), fileType);\n return;\n }\n\n if (fileType === \"video\" && videoMaxDurationSec) {\n const ok = await validateMediaDuration(file, videoMaxDurationSec);\n if (!ok) {\n emitError(err(\"too_long\", `Video too long. Max ${videoMaxDurationSec} seconds.`), fileType);\n return;\n }\n }\n\n if (fileType === \"audio\" && audioMaxDurationSec) {\n const ok = await validateMediaDuration(file, audioMaxDurationSec);\n if (!ok) {\n emitError(err(\"too_long\", `Audio too long. Max ${audioMaxDurationSec} seconds.`), fileType);\n return;\n }\n }\n\n // cropping path\n if (cropConfig && fileType === \"image\") {\n const reader = new FileReader();\n reader.onload = () => {\n setImageToCrop(reader.result as string);\n setIsCropperOpen(true);\n };\n reader.onerror = () => {\n emitError(err(\"read_failed\", \"Failed to read image for cropping.\"));\n };\n reader.readAsDataURL(file);\n setInternalSelected(file);\n return;\n }\n\n // normal selection\n setInternalSelected(file);\n const previewUrl = makePreviewUrl(file);\n onChange({ type: fileType, file, previewUrl, revokePreviewUrl: revokeLastPreviewUrl });\n }, [acceptTypes, maxSizeMB, cropConfig, videoMaxDurationSec, audioMaxDurationSec, emitError, makePreviewUrl, onChange, revokeLastPreviewUrl]);\n\n const handleCropComplete = (blob: Blob | null) => {\n setIsCropperOpen(false);\n\n const original = internalSelected;\n setImageToCrop(null);\n\n if (!blob) {\n emitError(err(\"crop_failed\", \"Cropping failed.\"), \"image\");\n return;\n }\n\n onChange({\n type: \"image\",\n file: original ?? undefined,\n blob,\n previewUrl: makePreviewUrl(blob),\n revokePreviewUrl: revokeLastPreviewUrl,\n });\n };\n\n const isUploading = typeof uploadProgress === \"number\" && isLoading;\n\n return (\n <div className={cn(\"relative w-full\", className)}>\n {localError && (\n <Alert variant=\"destructive\" className=\"mb-2\">\n <AlertTriangle className=\"icon-xs\" />\n <AlertDescription>{localError.message}</AlertDescription>\n </Alert>\n )}\n\n <Input\n id={inputId}\n type=\"file\"\n accept={acceptString}\n onChange={handleFileChange}\n className=\"hidden\"\n disabled={disabled || isLoading}\n ref={fileInputRef}\n />\n\n <div className=\"flex items-center gap-2\">\n <label htmlFor={inputId} className=\"w-full flex-grow\">\n <Button\n asChild\n disabled={disabled || isLoading}\n variant={variant}\n size={size}\n className={cn(\"w-full relative overflow-hidden cursor-pointer\", { \"icon-xl p-0\": size === \"icon\" })}\n >\n <span className=\"relative w-full h-full center-row\">\n {isUploading && (\n <Progress value={uploadProgress ?? 0} className=\"absolute left-0 top-0 h-full w-full z-0 bg-accent\" />\n )}\n\n <span className=\"z-10 center-row w-full\">\n {isLoading ? (\n <><Spinner size=\"xs\" className=\"mr-2\" /> {isUploading ? `${(uploadProgress ?? 0).toFixed(0)}%` : \"Processing...\"}</>\n ) : fileValue ? (\n <span className=\"flex items-center justify-between w-full\">\n <span className=\"truncate pr-2\">{getSelectedFileLabel(fileValue)}</span>\n <Button variant=\"ghost\" size=\"icon\" className=\"icon-sm hover:bg-destructive/20 shrink-0\" onClick={handleClear}>\n <X className=\"icon-xs\" />\n </Button>\n </span>\n ) : (\n <>\n {buttonLabel === \"Attach\"\n ? <><Paperclip className=\"mr-2 icon-xs\" /> {buttonLabel}</>\n : <>{getUploadIcon()} {buttonLabel}</>}\n </>\n )}\n </span>\n </span>\n </Button>\n </label>\n\n {detailsDescriptionParts.length > 0 && size !== \"icon\" && (\n <Button\n type=\"button\"\n variant=\"default\"\n size=\"icon\"\n onClick={() => setShowDetails((p) => !p)}\n aria-label=\"Show file processing details\"\n className=\"flex-shrink-0 icon-xl\"\n >\n <Info className=\"icon-sm\" />\n </Button>\n )}\n </div>\n\n {showDetails && detailsDescriptionParts.length > 0 && size !== \"icon\" && (\n <Card className=\"page-card w-full mt-2\">\n <div className=\"text-center p-2 space-y-0.5\">\n {detailsDescriptionParts.map((part, i) => (\n <p key={i} className=\"text-small font-bold\">{part}</p>\n ))}\n </div>\n </Card>\n )}\n\n {cropConfig && imageToCrop && (\n <ImageCropperModal\n isOpen={isCropperOpen}\n onClose={() => {\n setIsCropperOpen(false);\n setImageToCrop(null);\n }}\n imageSrc={imageToCrop}\n aspectRatio={cropConfig.aspectRatio}\n shape={cropConfig.shape}\n outputWidth={cropConfig.outputWidth}\n outputHeight={cropConfig.outputHeight}\n onCropComplete={handleCropComplete}\n />\n )}\n </div>\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-cropper-modal.d.ts","sourceRoot":"","sources":["../../../src/react/components/image-cropper-modal.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAgC,MAAM,OAAO,CAAC;AAOrD,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,CAAC,WAAW,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;CACpD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,
|
|
1
|
+
{"version":3,"file":"image-cropper-modal.d.ts","sourceRoot":"","sources":["../../../src/react/components/image-cropper-modal.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAgC,MAAM,OAAO,CAAC;AAOrD,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,CAAC,WAAW,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;CACpD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,4BA2D9D"}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useCallback } from "react";
|
|
4
4
|
import Cropper from "react-easy-crop";
|
|
5
5
|
const CropperComponent = Cropper;
|
|
6
|
-
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, Slider } from "@ttt-productions/ui-core/react";
|
|
6
|
+
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, Slider, useAsyncAction } from "@ttt-productions/ui-core/react";
|
|
7
7
|
import { getCroppedImg } from "../../lib/image-utils.js";
|
|
8
8
|
export function ImageCropperModal(props) {
|
|
9
9
|
const { isOpen, onClose, imageSrc, aspectRatio, shape, outputWidth, outputHeight, onCropComplete } = props;
|
|
@@ -13,20 +13,17 @@ export function ImageCropperModal(props) {
|
|
|
13
13
|
const handleCropComplete = useCallback((_a, pixels) => {
|
|
14
14
|
setCroppedAreaPixels(pixels);
|
|
15
15
|
}, []);
|
|
16
|
-
|
|
16
|
+
// Confirm renders the crop, then emits it. It is pending for that whole window and
|
|
17
|
+
// ignores a repeat click, so a double click can never emit (and upload) twice.
|
|
18
|
+
const confirm = useAsyncAction(async () => {
|
|
17
19
|
if (!imageSrc || !croppedAreaPixels)
|
|
18
20
|
return;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
onCropComplete(null);
|
|
26
|
-
}
|
|
27
|
-
}, [imageSrc, croppedAreaPixels, onCropComplete, onClose, outputWidth, outputHeight]);
|
|
21
|
+
const blob = await getCroppedImg(imageSrc, croppedAreaPixels, 0, { horizontal: false, vertical: false }, outputWidth, outputHeight);
|
|
22
|
+
onCropComplete(blob);
|
|
23
|
+
onClose();
|
|
24
|
+
}, { onError: () => onCropComplete(null) });
|
|
28
25
|
if (!imageSrc)
|
|
29
26
|
return null;
|
|
30
|
-
return (_jsx(Dialog, { open: isOpen, onOpenChange: (open) => !open && onClose(), children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Crop Image" }), _jsx(DialogDescription, { children: "Adjust the crop area and zoom level to select the desired portion of your image." })] }), _jsx("div", { className: "relative h-64 w-full bg-muted", children: _jsx(CropperComponent, { image: imageSrc, crop: crop, zoom: zoom, aspect: aspectRatio, cropShape: shape, showGrid: false, onCropChange: setCrop, onZoomChange: setZoom, onCropComplete: handleCropComplete }) }), _jsxs("div", { className: "stack-2", children: [_jsx("label", { htmlFor: "zoom", className: "text-small", children: "Zoom" }), _jsx(Slider, { id: "zoom", min: 1, max: 3, step: 0.1, value: [zoom], onValueChange: (v) => setZoom(v[0]) })] }), _jsxs(DialogFooter, { className: "flex-row justify-end gap-2", children: [_jsx(Button, { variant: "destructive", onClick: onClose, children: "Cancel" }), _jsx(Button, { variant: "default", onClick:
|
|
27
|
+
return (_jsx(Dialog, { open: isOpen, onOpenChange: (open) => !open && !confirm.pending && onClose(), children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Crop Image" }), _jsx(DialogDescription, { children: "Adjust the crop area and zoom level to select the desired portion of your image." })] }), _jsx("div", { className: "relative h-64 w-full bg-muted", children: _jsx(CropperComponent, { image: imageSrc, crop: crop, zoom: zoom, aspect: aspectRatio, cropShape: shape, showGrid: false, onCropChange: setCrop, onZoomChange: setZoom, onCropComplete: handleCropComplete }) }), _jsxs("div", { className: "stack-2", children: [_jsx("label", { htmlFor: "zoom", className: "text-small", children: "Zoom" }), _jsx(Slider, { id: "zoom", min: 1, max: 3, step: 0.1, value: [zoom], onValueChange: (v) => setZoom(v[0]) })] }), _jsxs(DialogFooter, { className: "flex-row justify-end gap-2", children: [_jsx(Button, { variant: "destructive", onClick: onClose, disabled: confirm.pending, children: "Cancel" }), _jsx(Button, { variant: "default", onClick: () => void confirm.run(), pending: confirm.pending, children: "Confirm" })] })] }) }));
|
|
31
28
|
}
|
|
32
29
|
//# sourceMappingURL=image-cropper-modal.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-cropper-modal.js","sourceRoot":"","sources":["../../../src/react/components/image-cropper-modal.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAc,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACrD,OAAO,OAAsB,MAAM,iBAAiB,CAAC;AAErD,MAAM,gBAAgB,GAAG,OAA8C,CAAC;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"image-cropper-modal.js","sourceRoot":"","sources":["../../../src/react/components/image-cropper-modal.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport Cropper, { type Area } from \"react-easy-crop\";\n \nconst CropperComponent = Cropper as unknown as React.ComponentType<any>;\nimport { Button, Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, Slider, useAsyncAction } from \"@ttt-productions/ui-core/react\";\nimport { getCroppedImg } from \"../../lib/image-utils.js\";\n\nexport interface ImageCropperModalProps {\n isOpen: boolean;\n onClose: () => void;\n imageSrc: string | null;\n aspectRatio: number;\n shape: \"rect\" | \"round\";\n outputWidth: number;\n outputHeight: number;\n onCropComplete: (croppedBlob: Blob | null) => void;\n}\n\nexport function ImageCropperModal(props: ImageCropperModalProps) {\n const { isOpen, onClose, imageSrc, aspectRatio, shape, outputWidth, outputHeight, onCropComplete } = props;\n\n const [crop, setCrop] = useState({ x: 0, y: 0 });\n const [zoom, setZoom] = useState(1);\n const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);\n\n const handleCropComplete = useCallback((_a: Area, pixels: Area) => {\n setCroppedAreaPixels(pixels);\n }, []);\n\n // Confirm renders the crop, then emits it. It is pending for that whole window and\n // ignores a repeat click, so a double click can never emit (and upload) twice.\n const confirm = useAsyncAction(\n async () => {\n if (!imageSrc || !croppedAreaPixels) return;\n const blob = await getCroppedImg(imageSrc, croppedAreaPixels, 0, { horizontal: false, vertical: false }, outputWidth, outputHeight);\n onCropComplete(blob);\n onClose();\n },\n { onError: () => onCropComplete(null) },\n );\n\n if (!imageSrc) return null;\n\n return (\n <Dialog open={isOpen} onOpenChange={(open) => !open && !confirm.pending && onClose()}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>Crop Image</DialogTitle>\n <DialogDescription>Adjust the crop area and zoom level to select the desired portion of your image.</DialogDescription>\n </DialogHeader>\n\n <div className=\"relative h-64 w-full bg-muted\">\n <CropperComponent\n image={imageSrc}\n crop={crop}\n zoom={zoom}\n aspect={aspectRatio}\n cropShape={shape}\n showGrid={false}\n onCropChange={setCrop}\n onZoomChange={setZoom}\n onCropComplete={handleCropComplete}\n />\n </div>\n\n <div className=\"stack-2\">\n <label htmlFor=\"zoom\" className=\"text-small\">Zoom</label>\n <Slider id=\"zoom\" min={1} max={3} step={0.1} value={[zoom]} onValueChange={(v) => setZoom(v[0])} />\n </div>\n\n <DialogFooter className=\"flex-row justify-end gap-2\">\n <Button variant=\"destructive\" onClick={onClose} disabled={confirm.pending}>Cancel</Button>\n <Button variant=\"default\" onClick={() => void confirm.run()} pending={confirm.pending}>Confirm</Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-input.d.ts","sourceRoot":"","sources":["../../../src/react/components/media-input.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAoG,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"media-input.d.ts","sourceRoot":"","sources":["../../../src/react/components/media-input.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAoG,MAAM,OAAO,CAAC;AAqBzH,OAAO,KAAK,EAAkB,gBAAgB,EAAE,eAAe,EAAqB,MAAM,gBAAgB,CAAC;AAoH3G,eAAO,MAAM,UAAU,0FA0oBrB,CAAC"}
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { getSimplifiedMediaType } from "@ttt-productions/media-schemas";
|
|
5
|
-
import { Info, Camera, Mic, Video, Upload, X,
|
|
6
|
-
import { Alert, AlertDescription, Button, Card, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Input, Progress, } from "@ttt-productions/ui-core/react";
|
|
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
7
|
import { cn } from "@ttt-productions/ui-core";
|
|
8
8
|
import { MediaPreview } from "@ttt-productions/media-viewer/react";
|
|
9
9
|
import { DEFAULT_PROGRESS_BAR_MIN_BYTES } from "../../index.js";
|
|
10
10
|
import { RecordDialog } from "./record-dialog.js";
|
|
11
|
-
import { ensureFileWithContentType
|
|
12
|
-
import { projectAcceptTokens } from "@ttt-productions/media-schemas";
|
|
11
|
+
import { ensureFileWithContentType } from "../../lib/infer-content-type.js";
|
|
12
|
+
import { NEUTRAL_CONTENT_TYPE, projectAcceptTokens } from "@ttt-productions/media-schemas";
|
|
13
13
|
import { ImageCropperModal } from "./image-cropper-modal.js";
|
|
14
14
|
import { MediaConstraintsHint } from "./media-constraints-hint.js";
|
|
15
15
|
import { PhotoCaptureModal } from "./photo-capture-modal.js";
|
|
@@ -139,6 +139,8 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
|
|
|
139
139
|
// via the trigger button; the imperative `openSelection()` handle opens it when
|
|
140
140
|
// more than one action is enabled.
|
|
141
141
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
142
|
+
const [validating, setValidating] = useState(false);
|
|
143
|
+
const [emittedSize, setEmittedSize] = useState(null);
|
|
142
144
|
// Prevent memory leaks from object URLs
|
|
143
145
|
const lastObjectUrlRef = useRef(null);
|
|
144
146
|
const revokeLastObjectUrl = useCallback(() => {
|
|
@@ -200,13 +202,15 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
|
|
|
200
202
|
const rawKind = payload.meta?.kind;
|
|
201
203
|
const mediaKind = rawKind === "image" || rawKind === "video" || rawKind === "audio" ? rawKind : "image";
|
|
202
204
|
const resolvedSpec = resolveActiveSpec(spec, mediaKind);
|
|
205
|
+
if (payload.file)
|
|
206
|
+
setEmittedSize(payload.file.size);
|
|
203
207
|
onChange({ spec: resolvedSpec, ...payload });
|
|
204
208
|
}, [onChange, spec]);
|
|
205
209
|
const fail = useCallback((e) => {
|
|
206
210
|
setLocalError(e);
|
|
207
211
|
emit({ error: e });
|
|
208
212
|
}, [emit]);
|
|
209
|
-
const
|
|
213
|
+
const validateAndEmit = useCallback(async (file, previewUrl, action = undefined) => {
|
|
210
214
|
setLocalError(null);
|
|
211
215
|
// Defense-in-depth: normalize the content type before any downstream
|
|
212
216
|
// handling. Recorder/capture files carry their declared kind; a picker
|
|
@@ -311,6 +315,18 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
|
|
|
311
315
|
}
|
|
312
316
|
emit({ file: safeFile, previewUrl, meta, claim });
|
|
313
317
|
}, [spec, cropSpec, emit, fail]);
|
|
318
|
+
// A picked, captured, or recorded file is validated (type, meta, duration,
|
|
319
|
+
// dimensions, crop) before it is emitted. The trigger shows pending for that
|
|
320
|
+
// whole window, so a second pick cannot race the first.
|
|
321
|
+
const handleSelected = useCallback(async (file, previewUrl, action = undefined) => {
|
|
322
|
+
setValidating(true);
|
|
323
|
+
try {
|
|
324
|
+
await validateAndEmit(file, previewUrl, action);
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
setValidating(false);
|
|
328
|
+
}
|
|
329
|
+
}, [validateAndEmit]);
|
|
314
330
|
const onPick = useCallback(async (e) => {
|
|
315
331
|
const file = e.target.files?.[0];
|
|
316
332
|
e.target.value = "";
|
|
@@ -355,10 +371,13 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
|
|
|
355
371
|
const isPreparing = phase === 'preparing' && isLoading;
|
|
356
372
|
const isFinalizing = phase === 'finalizing' && isLoading;
|
|
357
373
|
const sizeThreshold = progressBarMinBytes ?? DEFAULT_PROGRESS_BAR_MIN_BYTES;
|
|
374
|
+
// Consumers usually pass selectedFile={null} once the file is handed off, so
|
|
375
|
+
// the bar judges the size of the file this input emitted.
|
|
376
|
+
const trackedSize = selectedFile?.size ?? emittedSize;
|
|
358
377
|
const showRealBar = isUploading &&
|
|
359
378
|
typeof percent === 'number' &&
|
|
360
|
-
|
|
361
|
-
|
|
379
|
+
trackedSize !== null &&
|
|
380
|
+
trackedSize >= sizeThreshold;
|
|
362
381
|
const canPick = spec.client?.allowPick ?? true;
|
|
363
382
|
const canPhoto = spec.client?.allowCapturePhoto ?? true;
|
|
364
383
|
const canRecVideo = spec.client?.allowRecordVideo ?? true;
|
|
@@ -408,13 +427,13 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
|
|
|
408
427
|
const actions = useMemo(() => {
|
|
409
428
|
const list = [];
|
|
410
429
|
if (canPick)
|
|
411
|
-
list.push({ kind: "pick", label: "Choose file", icon: _jsx(Upload, { className: "
|
|
430
|
+
list.push({ kind: "pick", label: "Choose file", icon: _jsx(Upload, { className: "icon-xs" }) });
|
|
412
431
|
if (canPhoto)
|
|
413
|
-
list.push({ kind: "photo", label: "Take photo", icon: _jsx(Camera, { className: "
|
|
432
|
+
list.push({ kind: "photo", label: "Take photo", icon: _jsx(Camera, { className: "icon-xs" }) });
|
|
414
433
|
if (canRecVideo)
|
|
415
|
-
list.push({ kind: "recordVideo", label: "Record video", icon: _jsx(Video, { className: "
|
|
434
|
+
list.push({ kind: "recordVideo", label: "Record video", icon: _jsx(Video, { className: "icon-xs" }) });
|
|
416
435
|
if (canRecAudio)
|
|
417
|
-
list.push({ kind: "recordAudio", label: "Record audio", icon: _jsx(Mic, { className: "
|
|
436
|
+
list.push({ kind: "recordAudio", label: "Record audio", icon: _jsx(Mic, { className: "icon-xs" }) });
|
|
418
437
|
return list;
|
|
419
438
|
}, [canPick, canPhoto, canRecVideo, canRecAudio]);
|
|
420
439
|
/** Label & icon for the single trigger button. */
|
|
@@ -422,8 +441,8 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
|
|
|
422
441
|
if (actions.length === 1)
|
|
423
442
|
return actions[0].icon;
|
|
424
443
|
if (buttonLabel !== "Add media")
|
|
425
|
-
return _jsx(Upload, { className: "
|
|
426
|
-
return _jsx(Plus, { className: "
|
|
444
|
+
return _jsx(Upload, { className: "icon-xs" });
|
|
445
|
+
return _jsx(Plus, { className: "icon-xs" });
|
|
427
446
|
}, [actions, buttonLabel]);
|
|
428
447
|
const triggerLabel = useMemo(() => {
|
|
429
448
|
// If the consumer explicitly set a custom buttonLabel, always respect it.
|
|
@@ -449,16 +468,16 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
|
|
|
449
468
|
* Never touches the hidden input directly, so none of the gates are bypassed.
|
|
450
469
|
*/
|
|
451
470
|
const openSelection = useCallback(() => {
|
|
452
|
-
if (disabled || isLoading || actions.length === 0)
|
|
471
|
+
if (disabled || isLoading || validating || actions.length === 0)
|
|
453
472
|
return;
|
|
454
473
|
if (actions.length === 1) {
|
|
455
474
|
void runAction(actions[0].kind);
|
|
456
475
|
return;
|
|
457
476
|
}
|
|
458
477
|
setMenuOpen(true);
|
|
459
|
-
}, [disabled, isLoading, actions, runAction]);
|
|
478
|
+
}, [disabled, isLoading, validating, actions, runAction]);
|
|
460
479
|
useImperativeHandle(ref, () => ({ openSelection }), [openSelection]);
|
|
461
|
-
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 }), 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, { variant: "ghost", size: "icon", className: "icon-sm hover:bg-destructive/20 shrink-0", onClick: onClear, children: _jsx(X, { className: "icon-xs" }) }))] })] })) : null, isLoading ? (_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(
|
|
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, { variant: "ghost", size: "icon", className: "icon-sm hover:bg-destructive/20 shrink-0", onClick: onClear, 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
|
|
462
481
|
? "Preparing..."
|
|
463
482
|
: isUploading && showRealBar
|
|
464
483
|
? `Uploading ${Math.round(percent ?? 0)}%...`
|
|
@@ -468,9 +487,9 @@ export const MediaInput = forwardRef(function MediaInput(props, ref) {
|
|
|
468
487
|
? "Finalizing..."
|
|
469
488
|
: "Finalizing..." }), (isPreparing || isUploading) && onCancel ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "icon-sm hover:bg-destructive/20 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 ? (
|
|
470
489
|
/* Single action (or none): plain button, no dropdown */
|
|
471
|
-
|
|
490
|
+
_jsx(Button, { variant: "default", onClick: handleTriggerClick, disabled: disabled || isLoading || actions.length === 0, pending: validating, icon: triggerIcon, children: triggerLabel })) : (
|
|
472
491
|
/* Multiple actions: dropdown menu */
|
|
473
|
-
_jsxs(DropdownMenu, { open: menuOpen, onOpenChange: setMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children:
|
|
492
|
+
_jsxs(DropdownMenu, { open: menuOpen, onOpenChange: setMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(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, { 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: () => {
|
|
474
493
|
setCropOpen(false);
|
|
475
494
|
setCropSrc(null);
|
|
476
495
|
}, imageSrc: cropSrc, aspectRatio: cropSpec.aspectRatio, shape: cropSpec.shape ?? "rect", outputWidth: cropSpec.outputWidth, outputHeight: cropSpec.outputHeight, 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) => {
|