@ttt-productions/file-input 0.11.16 → 0.11.17

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 (38) hide show
  1. package/dist/lib/image-utils.d.ts +0 -1
  2. package/dist/lib/image-utils.d.ts.map +1 -1
  3. package/dist/lib/image-utils.js +1 -2
  4. package/dist/lib/image-utils.js.map +1 -1
  5. package/dist/lib/infer-content-type.d.ts +0 -23
  6. package/dist/lib/infer-content-type.d.ts.map +1 -1
  7. package/dist/lib/infer-content-type.js +2 -56
  8. package/dist/lib/infer-content-type.js.map +1 -1
  9. package/dist/lib/validate-media-duration.d.ts.map +1 -1
  10. package/dist/lib/validate-media-duration.js +2 -7
  11. package/dist/lib/validate-media-duration.js.map +1 -1
  12. package/dist/lib/validate-upload-kind.d.ts +0 -34
  13. package/dist/lib/validate-upload-kind.d.ts.map +1 -1
  14. package/dist/lib/validate-upload-kind.js +0 -17
  15. package/dist/lib/validate-upload-kind.js.map +1 -1
  16. package/dist/react/components/file-input.d.ts.map +1 -1
  17. package/dist/react/components/file-input.js +0 -2
  18. package/dist/react/components/file-input.js.map +1 -1
  19. package/dist/react/components/image-cropper-modal.d.ts.map +1 -1
  20. package/dist/react/components/image-cropper-modal.js +0 -2
  21. package/dist/react/components/image-cropper-modal.js.map +1 -1
  22. package/dist/react/components/media-constraints-hint.d.ts.map +1 -1
  23. package/dist/react/components/media-constraints-hint.js +0 -1
  24. package/dist/react/components/media-constraints-hint.js.map +1 -1
  25. package/dist/react/components/media-input.d.ts.map +1 -1
  26. package/dist/react/components/media-input.js +4 -69
  27. package/dist/react/components/media-input.js.map +1 -1
  28. package/dist/react/components/photo-capture-modal.d.ts.map +1 -1
  29. package/dist/react/components/photo-capture-modal.js +1 -5
  30. package/dist/react/components/photo-capture-modal.js.map +1 -1
  31. package/dist/react/components/record-dialog.d.ts +0 -4
  32. package/dist/react/components/record-dialog.d.ts.map +1 -1
  33. package/dist/react/components/record-dialog.js +11 -88
  34. package/dist/react/components/record-dialog.js.map +1 -1
  35. package/dist/types.d.ts +0 -56
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js.map +1 -1
  38. package/package.json +4 -4
@@ -1,6 +1,5 @@
1
1
  import type { Area } from 'react-easy-crop';
2
2
  import { type MediaCropSpec } from '@ttt-productions/media-schemas';
3
- /** How a crop is encoded; the fields an origin's crop spec declares. */
4
3
  export type CropEncoding = Pick<MediaCropSpec, 'format' | 'quality'>;
5
4
  export declare function getCroppedImg(imageSrc: string, pixelCrop: Area, rotation?: number, flip?: {
6
5
  horizontal: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"image-utils.d.ts","sourceRoot":"","sources":["../../src/lib/image-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAEnF,wEAAwE;AACxE,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,SAAS,CAAC,CAAC;AA0BrE,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,IAAI,EACf,QAAQ,SAAI,EACZ,IAAI;;;CAAyC,EAC7C,WAAW,CAAC,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,EACrB,QAAQ,GAAE,YAAiB,GAC1B,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAiDtB"}
1
+ {"version":3,"file":"image-utils.d.ts","sourceRoot":"","sources":["../../src/lib/image-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAEnF,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,SAAS,CAAC,CAAC;AA0BrE,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,IAAI,EACf,QAAQ,SAAI,EACZ,IAAI;;;CAAyC,EAC7C,WAAW,CAAC,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,EACrB,QAAQ,GAAE,YAAiB,GAC1B,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAgDtB"}
@@ -40,8 +40,7 @@ export async function getCroppedImg(imageSrc, pixelCrop, rotation = 0, flip = {
40
40
  croppedCanvas.width = outputWidth || pixelCrop.width;
41
41
  croppedCanvas.height = outputHeight || pixelCrop.height;
42
42
  croppedCtx.drawImage(canvas, pixelCrop.x, pixelCrop.y, pixelCrop.width, pixelCrop.height, 0, 0, croppedCanvas.width, croppedCanvas.height);
43
- // This is the first lossy encode the image gets, so it uses the origin's declared format and
44
- // quality; a browser that cannot encode the format returns PNG, and the blob's type says so.
43
+ // A browser that cannot encode the requested format returns PNG; the blob's type says which.
45
44
  const mime = MEDIA_FORMATS[encoding.format ?? DEFAULT_CROP_FORMAT].mimes.all[0];
46
45
  const quality = (encoding.quality ?? DEFAULT_CROP_QUALITY) / 100;
47
46
  return new Promise((resolve, reject) => {
@@ -1 +1 @@
1
- {"version":3,"file":"image-utils.js","sourceRoot":"","sources":["../../src/lib/image-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Area } from 'react-easy-crop';\nimport { MEDIA_FORMATS, type MediaCropSpec } from '@ttt-productions/media-schemas';\n\n/** How a crop is encoded; the fields an origin's crop spec declares. */\nexport type CropEncoding = Pick<MediaCropSpec, 'format' | 'quality'>;\n\nconst DEFAULT_CROP_FORMAT: NonNullable<MediaCropSpec['format']> = 'jpeg';\nconst DEFAULT_CROP_QUALITY = 80;\n\nconst createImage = (url: string): Promise<HTMLImageElement> =>\n new Promise((resolve, reject) => {\n const image = new Image();\n image.addEventListener('load', () => resolve(image));\n image.addEventListener('error', (error) => reject(error));\n image.setAttribute('crossOrigin', 'anonymous');\n image.src = url;\n });\n\nfunction getRadianAngle(deg: number) {\n return (deg * Math.PI) / 180;\n}\n\nfunction rotateSize(width: number, height: number, rotation: number) {\n const rotRad = getRadianAngle(rotation);\n return {\n width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),\n height: Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),\n };\n}\n\nexport async function getCroppedImg(\n imageSrc: string,\n pixelCrop: Area,\n rotation = 0,\n flip = { horizontal: false, vertical: false },\n outputWidth?: number,\n outputHeight?: number,\n encoding: CropEncoding = {},\n): Promise<Blob | null> {\n const image = await createImage(imageSrc);\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n\n const rotRad = getRadianAngle(rotation);\n const { width: bBoxWidth, height: bBoxHeight } = rotateSize(image.width, image.height, rotation);\n\n canvas.width = bBoxWidth;\n canvas.height = bBoxHeight;\n\n ctx.translate(bBoxWidth / 2, bBoxHeight / 2);\n ctx.rotate(rotRad);\n ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);\n ctx.translate(-image.width / 2, -image.height / 2);\n ctx.drawImage(image, 0, 0);\n\n const croppedCanvas = document.createElement('canvas');\n const croppedCtx = croppedCanvas.getContext('2d');\n if (!croppedCtx) return null;\n\n croppedCanvas.width = outputWidth || pixelCrop.width;\n croppedCanvas.height = outputHeight || pixelCrop.height;\n\n croppedCtx.drawImage(\n canvas,\n pixelCrop.x,\n pixelCrop.y,\n pixelCrop.width,\n pixelCrop.height,\n 0,\n 0,\n croppedCanvas.width,\n croppedCanvas.height,\n );\n\n // This is the first lossy encode the image gets, so it uses the origin's declared format and\n // quality; a browser that cannot encode the format returns PNG, and the blob's type says so.\n const mime = MEDIA_FORMATS[encoding.format ?? DEFAULT_CROP_FORMAT].mimes.all![0];\n const quality = (encoding.quality ?? DEFAULT_CROP_QUALITY) / 100;\n\n return new Promise((resolve, reject) => {\n croppedCanvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error('Canvas to Blob conversion failed'))),\n mime,\n quality,\n );\n });\n}\n"]}
1
+ {"version":3,"file":"image-utils.js","sourceRoot":"","sources":["../../src/lib/image-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Area } from 'react-easy-crop';\nimport { MEDIA_FORMATS, type MediaCropSpec } from '@ttt-productions/media-schemas';\n\nexport type CropEncoding = Pick<MediaCropSpec, 'format' | 'quality'>;\n\nconst DEFAULT_CROP_FORMAT: NonNullable<MediaCropSpec['format']> = 'jpeg';\nconst DEFAULT_CROP_QUALITY = 80;\n\nconst createImage = (url: string): Promise<HTMLImageElement> =>\n new Promise((resolve, reject) => {\n const image = new Image();\n image.addEventListener('load', () => resolve(image));\n image.addEventListener('error', (error) => reject(error));\n image.setAttribute('crossOrigin', 'anonymous');\n image.src = url;\n });\n\nfunction getRadianAngle(deg: number) {\n return (deg * Math.PI) / 180;\n}\n\nfunction rotateSize(width: number, height: number, rotation: number) {\n const rotRad = getRadianAngle(rotation);\n return {\n width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),\n height: Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),\n };\n}\n\nexport async function getCroppedImg(\n imageSrc: string,\n pixelCrop: Area,\n rotation = 0,\n flip = { horizontal: false, vertical: false },\n outputWidth?: number,\n outputHeight?: number,\n encoding: CropEncoding = {},\n): Promise<Blob | null> {\n const image = await createImage(imageSrc);\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n\n const rotRad = getRadianAngle(rotation);\n const { width: bBoxWidth, height: bBoxHeight } = rotateSize(image.width, image.height, rotation);\n\n canvas.width = bBoxWidth;\n canvas.height = bBoxHeight;\n\n ctx.translate(bBoxWidth / 2, bBoxHeight / 2);\n ctx.rotate(rotRad);\n ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);\n ctx.translate(-image.width / 2, -image.height / 2);\n ctx.drawImage(image, 0, 0);\n\n const croppedCanvas = document.createElement('canvas');\n const croppedCtx = croppedCanvas.getContext('2d');\n if (!croppedCtx) return null;\n\n croppedCanvas.width = outputWidth || pixelCrop.width;\n croppedCanvas.height = outputHeight || pixelCrop.height;\n\n croppedCtx.drawImage(\n canvas,\n pixelCrop.x,\n pixelCrop.y,\n pixelCrop.width,\n pixelCrop.height,\n 0,\n 0,\n croppedCanvas.width,\n croppedCanvas.height,\n );\n\n // A browser that cannot encode the requested format returns PNG; the blob's type says which.\n const mime = MEDIA_FORMATS[encoding.format ?? DEFAULT_CROP_FORMAT].mimes.all![0];\n const quality = (encoding.quality ?? DEFAULT_CROP_QUALITY) / 100;\n\n return new Promise((resolve, reject) => {\n croppedCanvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error('Canvas to Blob conversion failed'))),\n mime,\n quality,\n );\n });\n}\n"]}
@@ -1,27 +1,4 @@
1
1
  export type InferKind = 'image' | 'video' | 'audio';
2
- /**
3
- * Returns a valid, parameter-less media MIME for the given File.
4
- *
5
- * Precedence:
6
- * 1. The base of file.type (parameters stripped, lowercased) when valid —
7
- * unless `fallbackKind` is supplied and disagrees with the declared
8
- * top-level kind. A caller-declared kind (e.g. the record dialog's chosen
9
- * recording kind) is authoritative context, so a conflicting declared type
10
- * is treated as untrustworthy and resolution falls through to the
11
- * extension.
12
- * 2. Extension lookup. For ambiguous containers (webm/ogg exist as both audio
13
- * and video) a supplied `fallbackKind` picks the matching side; unambiguous
14
- * extensions keep their single mapping.
15
- * 3. The kind default for `fallbackKind`; without one, the NEUTRAL type — an
16
- * unknown picker file is never rewritten into a false media fact.
17
- */
18
2
  export declare function inferContentType(file: File, fallbackKind?: InferKind): string;
19
- /**
20
- * Returns a File whose .type is exactly the valid, parameter-less media MIME
21
- * from inferContentType. If the original file already carries that exact
22
- * type, it is returned unchanged; otherwise (missing type, parameterized
23
- * type, wrong case, or a type overridden by fallbackKind precedence) the
24
- * bytes are wrapped in a new File with the normalized type.
25
- */
26
3
  export declare function ensureFileWithContentType(file: File, fallbackKind?: InferKind): File;
27
4
  //# sourceMappingURL=infer-content-type.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"infer-content-type.d.ts","sourceRoot":"","sources":["../../src/lib/infer-content-type.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAuDpD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,EAAE,SAAS,GAAG,MAAM,CAc7E;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,EAAE,SAAS,GAAG,IAAI,CAIpF"}
@@ -1,24 +1,7 @@
1
- // Defense-in-depth: guarantee every File we emit has a NORMALIZED, honest
2
- // content type. If file.type is missing or unusable, infer from the extension;
3
- // with a caller-declared kind (recorder/capture — the caller KNOWS what it
4
- // produced) fall back to that kind's default container.
5
- //
6
- // MediaRecorder (and some pickers) report parameterized types such as
7
- // "audio/webm;codecs=opus". Downstream storage rules validate a
8
- // parameter-less base type, so we always emit the base ("audio/webm"),
9
- // never the parameterized form.
10
- //
11
- // UNKNOWN PICKER METADATA IS NOT FABRICATED (canonical-upload-content-
12
- // classification, 2026-07-31): when there is no declared kind, no usable MIME,
13
- // and no known extension, the file passes through as the NEUTRAL
14
- // `application/octet-stream` so the server's canonical byte inspector — the
15
- // only classification authority — sees "unknown" as unknown. The previous
16
- // behavior defaulted such files to `image/jpeg`, which invented a false media
17
- // fact the whole pipeline then trusted. upload-core accepts the neutral type
18
- // only via its explicit opt-in (`allowNeutralContentType`).
1
+ // MediaRecorder and some pickers report parameterized types ("audio/webm;codecs=opus"), and
2
+ // downstream checks validate the parameter-less base, so only the base is ever emitted.
19
3
  import { NEUTRAL_CONTENT_TYPE } from '@ttt-productions/media-schemas';
20
4
  const EXT_TO_MIME = {
21
- // image
22
5
  jpg: 'image/jpeg',
23
6
  jpeg: 'image/jpeg',
24
7
  png: 'image/png',
@@ -27,13 +10,11 @@ const EXT_TO_MIME = {
27
10
  avif: 'image/avif',
28
11
  heic: 'image/heic',
29
12
  heif: 'image/heif',
30
- // video
31
13
  mp4: 'video/mp4',
32
14
  m4v: 'video/mp4',
33
15
  mov: 'video/quicktime',
34
16
  webm: 'video/webm',
35
17
  mkv: 'video/x-matroska',
36
- // audio
37
18
  mp3: 'audio/mpeg',
38
19
  m4a: 'audio/mp4',
39
20
  aac: 'audio/aac',
@@ -42,10 +23,6 @@ const EXT_TO_MIME = {
42
23
  oga: 'audio/ogg',
43
24
  flac: 'audio/flac',
44
25
  };
45
- // Containers that legitimately hold either audio or video. When the caller
46
- // declares a kind (e.g. the record dialog's chosen recording kind), that kind
47
- // picks the matching side; without a kind we keep the historical single-kind
48
- // guess in EXT_TO_MIME (webm → video, ogg → audio).
49
26
  const AMBIGUOUS_EXT_TO_MIME = {
50
27
  webm: { audio: 'audio/webm', video: 'video/webm' },
51
28
  ogg: { audio: 'audio/ogg', video: 'video/ogg' },
@@ -56,12 +33,6 @@ const KIND_DEFAULT = {
56
33
  audio: 'audio/webm',
57
34
  };
58
35
  const BASE_MEDIA_MIME_RE = /^(image|video|audio)\/[a-z0-9.+-]+$/;
59
- /**
60
- * Extracts the parameter-less base MIME ("audio/webm" from
61
- * "audio/webm;codecs=opus"), lowercased, when it is a usable media type.
62
- * Returns null for empty values, application/octet-stream, and anything
63
- * whose base is not image/*, video/*, or audio/*.
64
- */
65
36
  function parseBaseMime(mime) {
66
37
  if (!mime)
67
38
  return null;
@@ -79,22 +50,6 @@ function extOf(name) {
79
50
  return null;
80
51
  return name.slice(dot + 1).toLowerCase();
81
52
  }
82
- /**
83
- * Returns a valid, parameter-less media MIME for the given File.
84
- *
85
- * Precedence:
86
- * 1. The base of file.type (parameters stripped, lowercased) when valid —
87
- * unless `fallbackKind` is supplied and disagrees with the declared
88
- * top-level kind. A caller-declared kind (e.g. the record dialog's chosen
89
- * recording kind) is authoritative context, so a conflicting declared type
90
- * is treated as untrustworthy and resolution falls through to the
91
- * extension.
92
- * 2. Extension lookup. For ambiguous containers (webm/ogg exist as both audio
93
- * and video) a supplied `fallbackKind` picks the matching side; unambiguous
94
- * extensions keep their single mapping.
95
- * 3. The kind default for `fallbackKind`; without one, the NEUTRAL type — an
96
- * unknown picker file is never rewritten into a false media fact.
97
- */
98
53
  export function inferContentType(file, fallbackKind) {
99
54
  const base = parseBaseMime(file.type);
100
55
  if (base && (!fallbackKind || kindOfMime(base) === fallbackKind))
@@ -109,17 +64,8 @@ export function inferContentType(file, fallbackKind) {
109
64
  }
110
65
  if (fallbackKind)
111
66
  return KIND_DEFAULT[fallbackKind];
112
- // No declared kind, no usable MIME, no known extension: NEUTRAL, never a
113
- // fabricated media type. The server inspector owns classification.
114
67
  return NEUTRAL_CONTENT_TYPE;
115
68
  }
116
- /**
117
- * Returns a File whose .type is exactly the valid, parameter-less media MIME
118
- * from inferContentType. If the original file already carries that exact
119
- * type, it is returned unchanged; otherwise (missing type, parameterized
120
- * type, wrong case, or a type overridden by fallbackKind precedence) the
121
- * bytes are wrapped in a new File with the normalized type.
122
- */
123
69
  export function ensureFileWithContentType(file, fallbackKind) {
124
70
  const type = inferContentType(file, fallbackKind);
125
71
  if (file.type === type)
@@ -1 +1 @@
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"]}
1
+ {"version":3,"file":"infer-content-type.js","sourceRoot":"","sources":["../../src/lib/infer-content-type.ts"],"names":[],"mappings":"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","sourcesContent":["// MediaRecorder and some pickers report parameterized types (\"audio/webm;codecs=opus\"), and\n// downstream checks validate the parameter-less base, so only the base is ever emitted.\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 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 mp4: 'video/mp4',\n m4v: 'video/mp4',\n mov: 'video/quicktime',\n webm: 'video/webm',\n mkv: 'video/x-matroska',\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\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\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\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 return NEUTRAL_CONTENT_TYPE;\n}\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"]}
@@ -1 +1 @@
1
- {"version":3,"file":"validate-media-duration.d.ts","sourceRoot":"","sources":["../../src/lib/validate-media-duration.ts"],"names":[],"mappings":"AAEA,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAsChG"}
1
+ {"version":3,"file":"validate-media-duration.d.ts","sourceRoot":"","sources":["../../src/lib/validate-media-duration.ts"],"names":[],"mappings":"AAEA,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAiChG"}
@@ -19,18 +19,13 @@ export async function validateMediaDuration(file, maxDurationSec) {
19
19
  finish(el.duration <= maxDurationSec);
20
20
  return;
21
21
  }
22
- // Chromium MediaRecorder blobs (recorded audio/video) report duration
23
- // Infinity at loadedmetadata, so `Infinity <= max` would reject every
24
- // recording. Resolve the real duration via the shared seek workaround;
25
- // if it still cannot be determined, let the file through — the server
26
- // measures every file's length and refuses one over the limit or with
27
- // no length at all.
22
+ // Chromium MediaRecorder blobs report duration Infinity at loadedmetadata; the seek workaround
23
+ // learns the real one.
28
24
  void resolveInfiniteDuration(el).then((duration) => {
29
25
  finish(duration === null ? true : duration <= maxDurationSec);
30
26
  });
31
27
  };
32
28
  el.onerror = () => {
33
- // Fail open on client (backend can enforce again).
34
29
  finish(true);
35
30
  };
36
31
  el.src = URL.createObjectURL(file);
@@ -1 +1 @@
1
- {"version":3,"file":"validate-media-duration.js","sourceRoot":"","sources":["../../src/lib/validate-media-duration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAExE,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,IAAU,EAAE,cAAsB;IAC5E,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACtF,EAAE,CAAC,OAAO,GAAG,UAAU,CAAC;QAExB,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,MAAM,GAAG,CAAC,EAAW,EAAE,EAAE;YAC7B,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,IAAI,CAAC;gBACH,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;YAC9B,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;YACV,OAAO,CAAC,EAAE,CAAC,CAAC;QACd,CAAC,CAAC;QAEF,EAAE,CAAC,gBAAgB,GAAG,GAAG,EAAE;YACzB,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACjC,MAAM,CAAC,EAAE,CAAC,QAAQ,IAAI,cAAc,CAAC,CAAC;gBACtC,OAAO;YACT,CAAC;YACD,sEAAsE;YACtE,sEAAsE;YACtE,uEAAuE;YACvE,sEAAsE;YACtE,sEAAsE;YACtE,oBAAoB;YACpB,KAAK,uBAAuB,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;gBACjD,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,cAAc,CAAC,CAAC;YAChE,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE;YAChB,mDAAmD;YACnD,MAAM,CAAC,IAAI,CAAC,CAAC;QACf,CAAC,CAAC;QAEF,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { resolveInfiniteDuration } from '@ttt-productions/media-viewer';\n\nexport async function validateMediaDuration(file: File, maxDurationSec: number): Promise<boolean> {\n return new Promise((resolve) => {\n const el = document.createElement(file.type.startsWith('video/') ? 'video' : 'audio');\n el.preload = 'metadata';\n\n let settled = false;\n const finish = (ok: boolean) => {\n if (settled) return;\n settled = true;\n try {\n URL.revokeObjectURL(el.src);\n } catch {}\n resolve(ok);\n };\n\n el.onloadedmetadata = () => {\n if (Number.isFinite(el.duration)) {\n finish(el.duration <= maxDurationSec);\n return;\n }\n // Chromium MediaRecorder blobs (recorded audio/video) report duration\n // Infinity at loadedmetadata, so `Infinity <= max` would reject every\n // recording. Resolve the real duration via the shared seek workaround;\n // if it still cannot be determined, let the file through — the server\n // measures every file's length and refuses one over the limit or with\n // no length at all.\n void resolveInfiniteDuration(el).then((duration) => {\n finish(duration === null ? true : duration <= maxDurationSec);\n });\n };\n\n el.onerror = () => {\n // Fail open on client (backend can enforce again).\n finish(true);\n };\n\n el.src = URL.createObjectURL(file);\n });\n}\n"]}
1
+ {"version":3,"file":"validate-media-duration.js","sourceRoot":"","sources":["../../src/lib/validate-media-duration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAExE,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,IAAU,EAAE,cAAsB;IAC5E,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACtF,EAAE,CAAC,OAAO,GAAG,UAAU,CAAC;QAExB,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,MAAM,GAAG,CAAC,EAAW,EAAE,EAAE;YAC7B,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,IAAI,CAAC;gBACH,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;YAC9B,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;YACV,OAAO,CAAC,EAAE,CAAC,CAAC;QACd,CAAC,CAAC;QAEF,EAAE,CAAC,gBAAgB,GAAG,GAAG,EAAE;YACzB,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACjC,MAAM,CAAC,EAAE,CAAC,QAAQ,IAAI,cAAc,CAAC,CAAC;gBACtC,OAAO;YACT,CAAC;YACD,+FAA+F;YAC/F,uBAAuB;YACvB,KAAK,uBAAuB,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;gBACjD,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,cAAc,CAAC,CAAC;YAChE,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE;YAChB,MAAM,CAAC,IAAI,CAAC,CAAC;QACf,CAAC,CAAC;QAEF,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { resolveInfiniteDuration } from '@ttt-productions/media-viewer';\n\nexport async function validateMediaDuration(file: File, maxDurationSec: number): Promise<boolean> {\n return new Promise((resolve) => {\n const el = document.createElement(file.type.startsWith('video/') ? 'video' : 'audio');\n el.preload = 'metadata';\n\n let settled = false;\n const finish = (ok: boolean) => {\n if (settled) return;\n settled = true;\n try {\n URL.revokeObjectURL(el.src);\n } catch {}\n resolve(ok);\n };\n\n el.onloadedmetadata = () => {\n if (Number.isFinite(el.duration)) {\n finish(el.duration <= maxDurationSec);\n return;\n }\n // Chromium MediaRecorder blobs report duration Infinity at loadedmetadata; the seek workaround\n // learns the real one.\n void resolveInfiniteDuration(el).then((duration) => {\n finish(duration === null ? true : duration <= maxDurationSec);\n });\n };\n\n el.onerror = () => {\n finish(true);\n };\n\n el.src = URL.createObjectURL(file);\n });\n}\n"]}
@@ -1,17 +1,7 @@
1
1
  type SingleKind = 'image' | 'video' | 'audio';
2
2
  export type ExpectedKind = SingleKind | 'any' | readonly SingleKind[];
3
- /**
4
- * Diagnostic codes emitted by validateAndNormalizeUploadFile when validation
5
- * fails. Consumers can route these to monitoring/Sentry via the onError option.
6
- */
7
3
  export type ValidateUploadFileErrorCode = 'upload_kind_indeterminate' | 'upload_kind_mismatch';
8
4
  export interface ValidateUploadFileOptions {
9
- /**
10
- * Optional diagnostic callback fired before the function throws.
11
- * Useful for routing to Sentry / monitoring-core. The thrown Error
12
- * carries the user-facing message; this callback gets the machine
13
- * code + structured extras.
14
- */
15
5
  onError?: (code: ValidateUploadFileErrorCode, extra: {
16
6
  fileOrigin: string;
17
7
  expectedKind: string;
@@ -20,32 +10,8 @@ export interface ValidateUploadFileOptions {
20
10
  fileSize: number;
21
11
  fileType: string;
22
12
  }) => void;
23
- /**
24
- * FAIL-OPEN for indeterminate picker metadata: when the file's kind cannot
25
- * be determined from browser MIME/extension, pass it through with the
26
- * NEUTRAL `application/octet-stream` type (never a fabricated media type)
27
- * instead of throwing, so the server's canonical byte inspector — the only
28
- * classification authority — decides. The kind-mismatch check still throws
29
- * when the client metadata IS determinate. Requires the upload path to opt
30
- * into `allowNeutralContentType`.
31
- */
32
13
  allowIndeterminate?: boolean;
33
14
  }
34
- /**
35
- * Validates and normalizes a file for upload.
36
- *
37
- * - Throws a user-friendly Error if the file's kind cannot be determined
38
- * (unless `allowIndeterminate` fails open to the neutral content type).
39
- * - Throws a user-friendly Error if `expectedKind` is not 'any' and doesn't
40
- * match the detected kind.
41
- * - Otherwise returns a File with a guaranteed valid media contentType.
42
- *
43
- * Optional `onError` callback fires with a machine-readable code + structured
44
- * extras before the throw, so consumers can route to monitoring without this
45
- * package depending on any monitoring library.
46
- *
47
- * Use this at the top of every upload mutation, before uploadFileResumable.
48
- */
49
15
  export declare function validateAndNormalizeUploadFile(file: File, fileOrigin: string, expectedKind: ExpectedKind, options?: ValidateUploadFileOptions): File;
50
16
  export {};
51
17
  //# sourceMappingURL=validate-upload-kind.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate-upload-kind.d.ts","sourceRoot":"","sources":["../../src/lib/validate-upload-kind.ts"],"names":[],"mappings":"AAGA,KAAK,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAC9C,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,KAAK,GAAG,SAAS,UAAU,EAAE,CAAC;AAEtE;;;GAGG;AACH,MAAM,MAAM,2BAA2B,GAAG,2BAA2B,GAAG,sBAAsB,CAAC;AAE/F,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CACR,IAAI,EAAE,2BAA2B,EACjC,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;QACpC,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,KACE,IAAI,CAAC;IACV;;;;;;;;OAQG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAcD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,YAAY,EAC1B,OAAO,CAAC,EAAE,yBAAyB,GAClC,IAAI,CAqCN"}
1
+ {"version":3,"file":"validate-upload-kind.d.ts","sourceRoot":"","sources":["../../src/lib/validate-upload-kind.ts"],"names":[],"mappings":"AAGA,KAAK,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAC9C,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,KAAK,GAAG,SAAS,UAAU,EAAE,CAAC;AAEtE,MAAM,MAAM,2BAA2B,GAAG,2BAA2B,GAAG,sBAAsB,CAAC;AAE/F,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,CACR,IAAI,EAAE,2BAA2B,EACjC,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;QACpC,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,KACE,IAAI,CAAC;IACV,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAcD,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,YAAY,EAC1B,OAAO,CAAC,EAAE,yBAAyB,GAClC,IAAI,CAmCN"}
@@ -14,27 +14,10 @@ function matchesExpected(detected, expected) {
14
14
  return expected.includes(detected);
15
15
  return detected === expected;
16
16
  }
17
- /**
18
- * Validates and normalizes a file for upload.
19
- *
20
- * - Throws a user-friendly Error if the file's kind cannot be determined
21
- * (unless `allowIndeterminate` fails open to the neutral content type).
22
- * - Throws a user-friendly Error if `expectedKind` is not 'any' and doesn't
23
- * match the detected kind.
24
- * - Otherwise returns a File with a guaranteed valid media contentType.
25
- *
26
- * Optional `onError` callback fires with a machine-readable code + structured
27
- * extras before the throw, so consumers can route to monitoring without this
28
- * package depending on any monitoring library.
29
- *
30
- * Use this at the top of every upload mutation, before uploadFileResumable.
31
- */
32
17
  export function validateAndNormalizeUploadFile(file, fileOrigin, expectedKind, options) {
33
18
  const detected = getSimplifiedMediaType(file);
34
19
  if (detected === 'other') {
35
20
  if (options?.allowIndeterminate === true) {
36
- // No fallback kind: inferContentType resolves an unknown file to the
37
- // NEUTRAL type, never a fabricated media fact.
38
21
  return ensureFileWithContentType(file);
39
22
  }
40
23
  options?.onError?.('upload_kind_indeterminate', {
@@ -1 +1 @@
1
- {"version":3,"file":"validate-upload-kind.js","sourceRoot":"","sources":["../../src/lib/validate-upload-kind.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAyCpE,SAAS,gBAAgB,CAAC,QAAsB;IAC9C,IAAI,QAAQ,KAAK,KAAK;QAAE,OAAO,wBAAwB,CAAC;IACxD,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1D,OAAO,QAAkB,CAAC;AAC5B,CAAC;AAED,SAAS,eAAe,CAAC,QAAoB,EAAE,QAAsB;IACnE,IAAI,QAAQ,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;QAAE,OAAQ,QAAkC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC3F,OAAO,QAAQ,KAAK,QAAQ,CAAC;AAC/B,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,8BAA8B,CAC5C,IAAU,EACV,UAAkB,EAClB,YAA0B,EAC1B,OAAmC;IAEnC,MAAM,QAAQ,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;IAE9C,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,IAAI,OAAO,EAAE,kBAAkB,KAAK,IAAI,EAAE,CAAC;YACzC,qEAAqE;YACrE,+CAA+C;YAC/C,OAAO,yBAAyB,CAAC,IAAI,CAAC,CAAC;QACzC,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,CAAC,2BAA2B,EAAE;YAC9C,UAAU;YACV,YAAY,EAAE,gBAAgB,CAAC,YAAY,CAAC;YAC5C,YAAY,EAAE,QAAQ;YACtB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;SACpB,CAAC,CAAC;QACH,MAAM,IAAI,KAAK,CACb,oGAAoG,CACrG,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,eAAe,CAAC,QAAQ,EAAE,YAAY,CAAC,EAAE,CAAC;QAC7C,OAAO,EAAE,OAAO,EAAE,CAAC,sBAAsB,EAAE;YACzC,UAAU;YACV,YAAY,EAAE,gBAAgB,CAAC,YAAY,CAAC;YAC5C,YAAY,EAAE,QAAQ;YACtB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;SACpB,CAAC,CAAC;QACH,MAAM,IAAI,KAAK,CACb,0BAA0B,gBAAgB,CAAC,YAAY,CAAC,gCAAgC,QAAQ,GAAG,CACpG,CAAC;IACJ,CAAC;IAED,OAAO,yBAAyB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACnD,CAAC","sourcesContent":["import { getSimplifiedMediaType } from '@ttt-productions/media-schemas';\nimport { ensureFileWithContentType } from './infer-content-type.js';\n\ntype SingleKind = 'image' | 'video' | 'audio';\nexport type ExpectedKind = SingleKind | 'any' | readonly SingleKind[];\n\n/**\n * Diagnostic codes emitted by validateAndNormalizeUploadFile when validation\n * fails. Consumers can route these to monitoring/Sentry via the onError option.\n */\nexport type ValidateUploadFileErrorCode = 'upload_kind_indeterminate' | 'upload_kind_mismatch';\n\nexport interface ValidateUploadFileOptions {\n /**\n * Optional diagnostic callback fired before the function throws.\n * Useful for routing to Sentry / monitoring-core. The thrown Error\n * carries the user-facing message; this callback gets the machine\n * code + structured extras.\n */\n onError?: (\n code: ValidateUploadFileErrorCode,\n extra: {\n fileOrigin: string;\n expectedKind: string;\n detectedKind?: SingleKind | 'other';\n fileName: string;\n fileSize: number;\n fileType: string;\n },\n ) => void;\n /**\n * FAIL-OPEN for indeterminate picker metadata: when the file's kind cannot\n * be determined from browser MIME/extension, pass it through with the\n * NEUTRAL `application/octet-stream` type (never a fabricated media type)\n * instead of throwing, so the server's canonical byte inspector — the only\n * classification authority — decides. The kind-mismatch check still throws\n * when the client metadata IS determinate. Requires the upload path to opt\n * into `allowNeutralContentType`.\n */\n allowIndeterminate?: boolean;\n}\n\nfunction describeExpected(expected: ExpectedKind): string {\n if (expected === 'any') return 'image, video, or audio';\n if (Array.isArray(expected)) return expected.join(' or ');\n return expected as string;\n}\n\nfunction matchesExpected(detected: SingleKind, expected: ExpectedKind): boolean {\n if (expected === 'any') return true;\n if (Array.isArray(expected)) return (expected as readonly SingleKind[]).includes(detected);\n return detected === expected;\n}\n\n/**\n * Validates and normalizes a file for upload.\n *\n * - Throws a user-friendly Error if the file's kind cannot be determined\n * (unless `allowIndeterminate` fails open to the neutral content type).\n * - Throws a user-friendly Error if `expectedKind` is not 'any' and doesn't\n * match the detected kind.\n * - Otherwise returns a File with a guaranteed valid media contentType.\n *\n * Optional `onError` callback fires with a machine-readable code + structured\n * extras before the throw, so consumers can route to monitoring without this\n * package depending on any monitoring library.\n *\n * Use this at the top of every upload mutation, before uploadFileResumable.\n */\nexport function validateAndNormalizeUploadFile(\n file: File,\n fileOrigin: string,\n expectedKind: ExpectedKind,\n options?: ValidateUploadFileOptions,\n): File {\n const detected = getSimplifiedMediaType(file);\n\n if (detected === 'other') {\n if (options?.allowIndeterminate === true) {\n // No fallback kind: inferContentType resolves an unknown file to the\n // NEUTRAL type, never a fabricated media fact.\n return ensureFileWithContentType(file);\n }\n options?.onError?.('upload_kind_indeterminate', {\n fileOrigin,\n expectedKind: describeExpected(expectedKind),\n detectedKind: detected,\n fileName: file.name,\n fileSize: file.size,\n fileType: file.type,\n });\n throw new Error(\n 'Could not determine the file type. Please try a different file or re-export it in a common format.',\n );\n }\n\n if (!matchesExpected(detected, expectedKind)) {\n options?.onError?.('upload_kind_mismatch', {\n fileOrigin,\n expectedKind: describeExpected(expectedKind),\n detectedKind: detected,\n fileName: file.name,\n fileSize: file.size,\n fileType: file.type,\n });\n throw new Error(\n `This slot only accepts ${describeExpected(expectedKind)} files. The selected file is ${detected}.`,\n );\n }\n\n return ensureFileWithContentType(file, detected);\n}\n"]}
1
+ {"version":3,"file":"validate-upload-kind.js","sourceRoot":"","sources":["../../src/lib/validate-upload-kind.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAsBpE,SAAS,gBAAgB,CAAC,QAAsB;IAC9C,IAAI,QAAQ,KAAK,KAAK;QAAE,OAAO,wBAAwB,CAAC;IACxD,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1D,OAAO,QAAkB,CAAC;AAC5B,CAAC;AAED,SAAS,eAAe,CAAC,QAAoB,EAAE,QAAsB;IACnE,IAAI,QAAQ,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;QAAE,OAAQ,QAAkC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC3F,OAAO,QAAQ,KAAK,QAAQ,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,8BAA8B,CAC5C,IAAU,EACV,UAAkB,EAClB,YAA0B,EAC1B,OAAmC;IAEnC,MAAM,QAAQ,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;IAE9C,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,IAAI,OAAO,EAAE,kBAAkB,KAAK,IAAI,EAAE,CAAC;YACzC,OAAO,yBAAyB,CAAC,IAAI,CAAC,CAAC;QACzC,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,CAAC,2BAA2B,EAAE;YAC9C,UAAU;YACV,YAAY,EAAE,gBAAgB,CAAC,YAAY,CAAC;YAC5C,YAAY,EAAE,QAAQ;YACtB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;SACpB,CAAC,CAAC;QACH,MAAM,IAAI,KAAK,CACb,oGAAoG,CACrG,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,eAAe,CAAC,QAAQ,EAAE,YAAY,CAAC,EAAE,CAAC;QAC7C,OAAO,EAAE,OAAO,EAAE,CAAC,sBAAsB,EAAE;YACzC,UAAU;YACV,YAAY,EAAE,gBAAgB,CAAC,YAAY,CAAC;YAC5C,YAAY,EAAE,QAAQ;YACtB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;SACpB,CAAC,CAAC;QACH,MAAM,IAAI,KAAK,CACb,0BAA0B,gBAAgB,CAAC,YAAY,CAAC,gCAAgC,QAAQ,GAAG,CACpG,CAAC;IACJ,CAAC;IAED,OAAO,yBAAyB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACnD,CAAC","sourcesContent":["import { getSimplifiedMediaType } from '@ttt-productions/media-schemas';\nimport { ensureFileWithContentType } from './infer-content-type.js';\n\ntype SingleKind = 'image' | 'video' | 'audio';\nexport type ExpectedKind = SingleKind | 'any' | readonly SingleKind[];\n\nexport type ValidateUploadFileErrorCode = 'upload_kind_indeterminate' | 'upload_kind_mismatch';\n\nexport interface ValidateUploadFileOptions {\n onError?: (\n code: ValidateUploadFileErrorCode,\n extra: {\n fileOrigin: string;\n expectedKind: string;\n detectedKind?: SingleKind | 'other';\n fileName: string;\n fileSize: number;\n fileType: string;\n },\n ) => void;\n allowIndeterminate?: boolean;\n}\n\nfunction describeExpected(expected: ExpectedKind): string {\n if (expected === 'any') return 'image, video, or audio';\n if (Array.isArray(expected)) return expected.join(' or ');\n return expected as string;\n}\n\nfunction matchesExpected(detected: SingleKind, expected: ExpectedKind): boolean {\n if (expected === 'any') return true;\n if (Array.isArray(expected)) return (expected as readonly SingleKind[]).includes(detected);\n return detected === expected;\n}\n\nexport function validateAndNormalizeUploadFile(\n file: File,\n fileOrigin: string,\n expectedKind: ExpectedKind,\n options?: ValidateUploadFileOptions,\n): File {\n const detected = getSimplifiedMediaType(file);\n\n if (detected === 'other') {\n if (options?.allowIndeterminate === true) {\n return ensureFileWithContentType(file);\n }\n options?.onError?.('upload_kind_indeterminate', {\n fileOrigin,\n expectedKind: describeExpected(expectedKind),\n detectedKind: detected,\n fileName: file.name,\n fileSize: file.size,\n fileType: file.type,\n });\n throw new Error(\n 'Could not determine the file type. Please try a different file or re-export it in a common format.',\n );\n }\n\n if (!matchesExpected(detected, expectedKind)) {\n options?.onError?.('upload_kind_mismatch', {\n fileOrigin,\n expectedKind: describeExpected(expectedKind),\n detectedKind: detected,\n fileName: file.name,\n fileSize: file.size,\n fileType: file.type,\n });\n throw new Error(\n `This slot only accepts ${describeExpected(expectedKind)} files. The selected file is ${detected}.`,\n );\n }\n\n return ensureFileWithContentType(file, detected);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"file-input.d.ts","sourceRoot":"","sources":["../../../src/react/components/file-input.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAkB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAgCrE,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,+BA0V9C"}
1
+ {"version":3,"file":"file-input.d.ts","sourceRoot":"","sources":["../../../src/react/components/file-input.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAkB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAgCrE,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,+BAwV9C"}
@@ -138,7 +138,6 @@ export function FileInput(props) {
138
138
  return;
139
139
  }
140
140
  }
141
- // cropping path
142
141
  if (cropConfig && fileType === 'image') {
143
142
  const reader = new FileReader();
144
143
  reader.onload = () => {
@@ -152,7 +151,6 @@ export function FileInput(props) {
152
151
  setInternalSelected(file);
153
152
  return;
154
153
  }
155
- // normal selection
156
154
  setInternalSelected(file);
157
155
  const previewUrl = makePreviewUrl(file);
158
156
  onChange({ type: fileType, file, previewUrl, revokePreviewUrl: revokeLastPreviewUrl });
@@ -1 +1 @@
1
- {"version":3,"file":"file-input.js","sourceRoot":"","sources":["../../../src/react/components/file-input.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAA4B,MAAM,gCAAgC,CAAC;AAClG,OAAO,EACL,KAAK,EACL,gBAAgB,EAChB,MAAM,EACN,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,OAAO,GACR,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,EAAE,EAAE,MAAM,0BAA0B,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGtF,OAAO,EAAE,qBAAqB,EAAE,MAAM,sCAAsC,CAAC;AAC7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7D,MAAM,QAAQ,GAAwC;IACpD,KAAK,EAAE,oFAAoF;IAC3F,KAAK,EAAE,eAAe;IACtB,KAAK,EAAE,SAAS;IAChB,KAAK,EAAE,mBAAmB;CAC3B,CAAC;AAEF,SAAS,GAAG,CACV,IAA4B,EAC5B,OAAe,EACf,OAAiC;IAEjC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AACpC,CAAC;AAED,SAAS,oBAAoB,CAAC,IAAU;IACtC,QAAQ,sBAAsB,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,KAAK,OAAO;YACV,OAAO,gBAAgB,CAAC;QAC1B,KAAK,OAAO;YACV,OAAO,gBAAgB,CAAC;QAC1B,KAAK,OAAO;YACV,OAAO,gBAAgB,CAAC;QAC1B;YACE,OAAO,eAAe,CAAC;IAC3B,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAqB;IAC7C,MAAM,EACJ,WAAW,EACX,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,OAAO,EACP,SAAS,GAAG,KAAK,EACjB,QAAQ,GAAG,KAAK,EAChB,WAAW,GAAG,aAAa,EAC3B,UAAU,GAAG,qBAAqB,EAClC,SAAS,EACT,cAAc,GAAG,IAAI,EACrB,OAAO,GAAG,SAAS,EACnB,IAAI,GAAG,SAAS,EAChB,UAAU,EACV,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,GAAG,KAAK,GAC3B,GAAG,KAAK,CAAC;IAEV,MAAM,YAAY,GAAG,MAAM,CAAmB,IAAI,CAAC,CAAC;IACpD,MAAM,iBAAiB,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAC;IAEtD,MAAM,oBAAoB,GAAG,WAAW,CAAC,GAAG,EAAE;QAC5C,IAAI,CAAC,iBAAiB,CAAC,OAAO;YAAE,OAAO;QACvC,IAAI,CAAC;YACH,GAAG,CAAC,eAAe,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;QACjD,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QACV,iBAAiB,CAAC,OAAO,GAAG,IAAI,CAAC;IACnC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,cAAc,GAAG,WAAW,CAChC,CAAC,IAAU,EAAE,EAAE;QACb,oBAAoB,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;QACtC,iBAAiB,CAAC,OAAO,GAAG,GAAG,CAAC;QAChC,OAAO,GAAG,CAAC;IACb,CAAC,EACD,CAAC,oBAAoB,CAAC,CACvB,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,GAAG,EAAE,CAAC,oBAAoB,EAAE,CAAC;IACtC,CAAC,EAAE,CAAC,oBAAoB,CAAC,CAAC,CAAC;IAC3B,MAAM,QAAQ,GAAG,KAAK,EAAE,CAAC;IACzB,MAAM,OAAO,GAAG,cAAc,QAAQ,EAAE,CAAC;IAEzC,MAAM,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,GAAG,QAAQ,CAAc,IAAI,CAAC,CAAC;IAC5E,MAAM,SAAS,GAAG,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,gBAAgB,CAAC;IAE/E,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAwB,IAAI,CAAC,CAAC;IAE1E,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC1D,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IAEpE,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,kBAAkB,CAAC,CAAC;IAEnE,MAAM,YAAY,GAAG,OAAO,CAC1B,GAAG,EAAE,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,EACvF,CAAC,WAAW,CAAC,CACd,CAAC;IAEF,MAAM,uBAAuB,GAAG,OAAO,CAAC,GAAG,EAAE;QAC3C,MAAM,KAAK,GAAa,EAAE,CAAC;QAE3B,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,YAAY,GAChB,UAAU,CAAC,kBAAkB,IAAI,GAAG,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;YAC5E,KAAK,CAAC,IAAI,CACR,8BAA8B,YAAY,UAAU,UAAU,CAAC,WAAW,IAAI,UAAU,CAAC,YAAY,MAAM,CAC5G,CAAC;QACJ,CAAC;QAED,MAAM,SAAS,GAAG,WAAW;aAC1B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACT,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CACtF;aACA,MAAM,CAAC,OAAO,CAAa,CAAC;QAE/B,IAAI,SAAS,CAAC,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,cAAc,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAExE,IAAI,mBAAmB;YAAE,KAAK,CAAC,IAAI,CAAC,uBAAuB,mBAAmB,IAAI,CAAC,CAAC;QACpF,IAAI,mBAAmB;YAAE,KAAK,CAAC,IAAI,CAAC,uBAAuB,mBAAmB,IAAI,CAAC,CAAC;QAEpF,OAAO,KAAK,CAAC;IACf,CAAC,EAAE,CAAC,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,mBAAmB,EAAE,mBAAmB,CAAC,CAAC,CAAC;IAEnF,MAAM,aAAa,GAAG,GAAG,EAAE;QACzB,IAAI,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,KAAC,MAAM,IAAC,SAAS,EAAC,cAAc,GAAG,CAAC;QAC9E,IAAI,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,KAAC,IAAI,IAAC,SAAS,EAAC,cAAc,GAAG,CAAC;QAC5E,IAAI,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,KAAC,KAAK,IAAC,SAAS,EAAC,cAAc,GAAG,CAAC;QAC7E,OAAO,KAAC,MAAM,IAAC,SAAS,EAAC,cAAc,GAAG,CAAC;IAC7C,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,IAAI,YAAY,CAAC,OAAO;YAAE,YAAY,CAAC,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC;IAC5D,CAAC,CAAC;IAEF,MAAM,SAAS,GAAG,WAAW,CAC3B,CAAC,CAAiB,EAAE,OAA4B,OAAO,EAAE,EAAE;QACzD,aAAa,CAAC,CAAC,CAAC,CAAC;QACjB,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QACb,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/B,CAAC,EACD,CAAC,OAAO,EAAE,QAAQ,CAAC,CACpB,CAAC;IAEF,MAAM,WAAW,GAAG,CAAC,CAAsC,EAAE,EAAE;QAC7D,CAAC,CAAC,eAAe,EAAE,CAAC;QACpB,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,aAAa,CAAC,IAAI,CAAC,CAAC;QACpB,mBAAmB,CAAC,IAAI,CAAC,CAAC;QAC1B,WAAW,EAAE,CAAC;QACd,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAC9B,CAAC,CAAC;IAEF,MAAM,gBAAgB,GAAG,WAAW,CAClC,KAAK,EAAE,KAA0C,EAAE,EAAE;QACnD,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;QACrC,WAAW,EAAE,CAAC;QACd,aAAa,CAAC,IAAI,CAAC,CAAC;QAEpB,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,mBAAmB,CAAC,IAAI,CAAC,CAAC;YAC1B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;QAC9C,IAAI,WAAW,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1D,SAAS,CACP,GAAG,CAAC,cAAc,EAAE,+BAA+B,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAC7E,QAAQ,CACT,CAAC;YACF,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;QACjE,IAAI,IAAI,CAAC,IAAI,GAAG,QAAQ,EAAE,CAAC;YACzB,SAAS,CACP,GAAG,CAAC,WAAW,EAAE,uBAAuB,SAAS,CAAC,QAAQ,CAAC,UAAU,QAAQ,GAAG,EAAE;gBAChF,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,QAAQ;aACT,CAAC,EACF,QAAQ,CACT,CAAC;YACF,OAAO;QACT,CAAC;QAED,IAAI,QAAQ,KAAK,OAAO,IAAI,mBAAmB,EAAE,CAAC;YAChD,MAAM,EAAE,GAAG,MAAM,qBAAqB,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;YAClE,IAAI,CAAC,EAAE,EAAE,CAAC;gBACR,SAAS,CACP,GAAG,CAAC,UAAU,EAAE,uBAAuB,mBAAmB,WAAW,CAAC,EACtE,QAAQ,CACT,CAAC;gBACF,OAAO;YACT,CAAC;QACH,CAAC;QAED,IAAI,QAAQ,KAAK,OAAO,IAAI,mBAAmB,EAAE,CAAC;YAChD,MAAM,EAAE,GAAG,MAAM,qBAAqB,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;YAClE,IAAI,CAAC,EAAE,EAAE,CAAC;gBACR,SAAS,CACP,GAAG,CAAC,UAAU,EAAE,uBAAuB,mBAAmB,WAAW,CAAC,EACtE,QAAQ,CACT,CAAC;gBACF,OAAO;YACT,CAAC;QACH,CAAC;QAED,gBAAgB;QAChB,IAAI,UAAU,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;YAChC,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE;gBACnB,cAAc,CAAC,MAAM,CAAC,MAAgB,CAAC,CAAC;gBACxC,gBAAgB,CAAC,IAAI,CAAC,CAAC;YACzB,CAAC,CAAC;YACF,MAAM,CAAC,OAAO,GAAG,GAAG,EAAE;gBACpB,SAAS,CAAC,GAAG,CAAC,aAAa,EAAE,oCAAoC,CAAC,CAAC,CAAC;YACtE,CAAC,CAAC;YACF,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;YAC3B,mBAAmB,CAAC,IAAI,CAAC,CAAC;YAC1B,OAAO;QACT,CAAC;QAED,mBAAmB;QACnB,mBAAmB,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACxC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,CAAC,CAAC;IACzF,CAAC,EACD;QACE,WAAW;QACX,SAAS;QACT,UAAU;QACV,mBAAmB;QACnB,mBAAmB;QACnB,SAAS;QACT,cAAc;QACd,QAAQ;QACR,oBAAoB;KACrB,CACF,CAAC;IAEF,MAAM,kBAAkB,GAAG,CAAC,IAAiB,EAAE,EAAE;QAC/C,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAExB,MAAM,QAAQ,GAAG,gBAAgB,CAAC;QAClC,cAAc,CAAC,IAAI,CAAC,CAAC;QAErB,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,SAAS,CAAC,GAAG,CAAC,aAAa,EAAE,kBAAkB,CAAC,EAAE,OAAO,CAAC,CAAC;YAC3D,OAAO;QACT,CAAC;QAED,QAAQ,CAAC;YACP,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,QAAQ,IAAI,SAAS;YAC3B,IAAI;YACJ,UAAU,EAAE,cAAc,CAAC,IAAI,CAAC;YAChC,gBAAgB,EAAE,oBAAoB;SACvC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,OAAO,cAAc,KAAK,QAAQ,IAAI,SAAS,CAAC;IAEpE,OAAO,CACL,eAAK,SAAS,EAAE,EAAE,CAAC,iBAAiB,EAAE,SAAS,CAAC,aAC7C,UAAU,IAAI,CACb,MAAC,KAAK,IAAC,OAAO,EAAC,aAAa,EAAC,SAAS,EAAC,MAAM,aAC3C,KAAC,aAAa,IAAC,SAAS,EAAC,SAAS,GAAG,EACrC,KAAC,gBAAgB,cAAE,UAAU,CAAC,OAAO,GAAoB,IACnD,CACT,EAED,KAAC,KAAK,IACJ,EAAE,EAAE,OAAO,EACX,IAAI,EAAC,MAAM,EACX,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,EAAC,QAAQ,EAClB,QAAQ,EAAE,QAAQ,IAAI,SAAS,EAC/B,GAAG,EAAE,YAAY,GACjB,EAEF,eAAK,SAAS,EAAC,yBAAyB,aACtC,gBAAO,OAAO,EAAE,OAAO,EAAE,SAAS,EAAC,kBAAkB,YACnD,KAAC,MAAM,IACL,OAAO,QACP,QAAQ,EAAE,QAAQ,IAAI,SAAS,EAC/B,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,EAAE,CAAC,gDAAgD,EAAE;gCAC9D,aAAa,EAAE,IAAI,KAAK,MAAM;6BAC/B,CAAC,YAEF,gBAAM,SAAS,EAAC,mCAAmC,aAChD,WAAW,IAAI,CACd,KAAC,QAAQ,IACP,KAAK,EAAE,cAAc,IAAI,CAAC,EAC1B,SAAS,EAAC,mDAAmD,GAC7D,CACH,EAED,eAAM,SAAS,EAAC,wBAAwB,YACrC,SAAS,CAAC,CAAC,CAAC,CACX,8BACE,KAAC,OAAO,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,MAAM,GAAG,EAAC,GAAG,EACzC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,cAAc,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,eAAe,IACtE,CACJ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CACd,gBAAM,SAAS,EAAC,0CAA0C,aACxD,eAAM,SAAS,EAAC,eAAe,YAAE,oBAAoB,CAAC,SAAS,CAAC,GAAQ,EACxE,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,MAAM,EACX,SAAS,EAAC,kBAAkB,EAC5B,OAAO,EAAE,WAAW,gBACR,UAAU,YAEtB,KAAC,CAAC,IAAC,SAAS,EAAC,SAAS,GAAG,GAClB,IACJ,CACR,CAAC,CAAC,CAAC,CACF,4BACG,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,CAC1B,8BACE,KAAC,SAAS,IAAC,SAAS,EAAC,cAAc,GAAG,OAAE,WAAW,IAClD,CACJ,CAAC,CAAC,CAAC,CACF,8BACG,aAAa,EAAE,OAAG,WAAW,IAC7B,CACJ,GACA,CACJ,GACI,IACF,GACA,GACH,EAEP,uBAAuB,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,KAAK,MAAM,IAAI,CACxD,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,MAAM,EACX,OAAO,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,gBAC7B,8BAA8B,EACzC,SAAS,EAAC,uBAAuB,YAEjC,KAAC,IAAI,IAAC,SAAS,EAAC,SAAS,GAAG,GACrB,CACV,IACG,EAEL,WAAW,IAAI,uBAAuB,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,KAAK,MAAM,IAAI,CACvE,KAAC,IAAI,IAAC,SAAS,EAAC,uBAAuB,YACrC,cAAK,SAAS,EAAC,6BAA6B,YACzC,uBAAuB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CACxC,YAAW,SAAS,EAAC,sBAAsB,YACxC,IAAI,IADC,CAAC,CAEL,CACL,CAAC,GACE,GACD,CACR,EAEA,UAAU,IAAI,WAAW,IAAI,CAC5B,KAAC,iBAAiB,IAChB,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,GAAG,EAAE;oBACZ,gBAAgB,CAAC,KAAK,CAAC,CAAC;oBACxB,cAAc,CAAC,IAAI,CAAC,CAAC;gBACvB,CAAC,EACD,QAAQ,EAAE,WAAW,EACrB,WAAW,EAAE,UAAU,CAAC,WAAW,EACnC,KAAK,EAAE,UAAU,CAAC,KAAK,EACvB,WAAW,EAAE,UAAU,CAAC,WAAW,EACnC,YAAY,EAAE,UAAU,CAAC,YAAY,EACrC,cAAc,EAAE,kBAAkB,GAClC,CACH,IACG,CACP,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useMemo, useRef, useState, useId } from 'react';\nimport { getSimplifiedMediaType, type SimplifiedMediaType } from '@ttt-productions/media-schemas';\nimport {\n Alert,\n AlertDescription,\n Button,\n Card,\n Input,\n Progress,\n Spinner,\n} 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(\n code: FileInputError['code'],\n message: string,\n details?: Record<string, unknown>,\n): FileInputError {\n return { code, message, details };\n}\n\nfunction getSelectedFileLabel(file: File): string {\n switch (getSimplifiedMediaType(file)) {\n case 'image':\n return 'Image selected';\n case 'video':\n return 'Video selected';\n case 'audio':\n return 'Audio selected';\n default:\n 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 clearLabel = 'Clear selected 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(\n (blob: Blob) => {\n revokeLastPreviewUrl();\n const url = URL.createObjectURL(blob);\n lastPreviewUrlRef.current = url;\n return url;\n },\n [revokeLastPreviewUrl],\n );\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 =\n cropConfig.aspectRatioDisplay || `${cropConfig.aspectRatio.toFixed(2)}:1`;\n parts.push(\n `Image will be cropped to a ${ratioDisplay} rect (${cropConfig.outputWidth}x${cropConfig.outputHeight}px).`,\n );\n }\n\n const sizeParts = acceptTypes\n .map((t) =>\n maxSizeMB[t] ? `${t.charAt(0).toUpperCase() + t.slice(1)} (${maxSizeMB[t]}MB)` : null,\n )\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(\n (e: FileInputError, type: SimplifiedMediaType = 'other') => {\n setLocalError(e);\n onError?.(e);\n onChange({ type, error: e });\n },\n [onError, onChange],\n );\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(\n 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(\n err('invalid_type', `Invalid file type. Allowed: ${acceptTypes.join(', ')}.`),\n fileType,\n );\n return;\n }\n\n const maxBytes = (maxSizeMB[fileType] ?? Infinity) * 1024 * 1024;\n if (file.size > maxBytes) {\n emitError(\n err('too_large', `File too large. Max ${maxSizeMB[fileType]}MB for ${fileType}.`, {\n size: file.size,\n maxBytes,\n }),\n fileType,\n );\n return;\n }\n\n if (fileType === 'video' && videoMaxDurationSec) {\n const ok = await validateMediaDuration(file, videoMaxDurationSec);\n if (!ok) {\n emitError(\n err('too_long', `Video too long. Max ${videoMaxDurationSec} seconds.`),\n fileType,\n );\n return;\n }\n }\n\n if (fileType === 'audio' && audioMaxDurationSec) {\n const ok = await validateMediaDuration(file, audioMaxDurationSec);\n if (!ok) {\n emitError(\n err('too_long', `Audio too long. Max ${audioMaxDurationSec} seconds.`),\n fileType,\n );\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 },\n [\n acceptTypes,\n maxSizeMB,\n cropConfig,\n videoMaxDurationSec,\n audioMaxDurationSec,\n emitError,\n makePreviewUrl,\n onChange,\n revokeLastPreviewUrl,\n ],\n );\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', {\n 'icon-xl p-0': size === 'icon',\n })}\n >\n <span className=\"relative w-full h-full center-row\">\n {isUploading && (\n <Progress\n value={uploadProgress ?? 0}\n className=\"absolute left-0 top-0 h-full w-full z-0 bg-accent\"\n />\n )}\n\n <span className=\"z-10 center-row w-full\">\n {isLoading ? (\n <>\n <Spinner size=\"xs\" className=\"mr-2\" />{' '}\n {isUploading ? `${(uploadProgress ?? 0).toFixed(0)}%` : 'Processing...'}\n </>\n ) : fileValue ? (\n <span className=\"flex items-center justify-between w-full\">\n <span className=\"truncate pr-2\">{getSelectedFileLabel(fileValue)}</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"icon-sm shrink-0\"\n onClick={handleClear}\n aria-label={clearLabel}\n >\n <X className=\"icon-xs\" />\n </Button>\n </span>\n ) : (\n <>\n {buttonLabel === 'Attach' ? (\n <>\n <Paperclip className=\"mr-2 icon-xs\" /> {buttonLabel}\n </>\n ) : (\n <>\n {getUploadIcon()} {buttonLabel}\n </>\n )}\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\">\n {part}\n </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 {\n Alert,\n AlertDescription,\n Button,\n Card,\n Input,\n Progress,\n Spinner,\n} 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(\n code: FileInputError['code'],\n message: string,\n details?: Record<string, unknown>,\n): FileInputError {\n return { code, message, details };\n}\n\nfunction getSelectedFileLabel(file: File): string {\n switch (getSimplifiedMediaType(file)) {\n case 'image':\n return 'Image selected';\n case 'video':\n return 'Video selected';\n case 'audio':\n return 'Audio selected';\n default:\n 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 clearLabel = 'Clear selected 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(\n (blob: Blob) => {\n revokeLastPreviewUrl();\n const url = URL.createObjectURL(blob);\n lastPreviewUrlRef.current = url;\n return url;\n },\n [revokeLastPreviewUrl],\n );\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 =\n cropConfig.aspectRatioDisplay || `${cropConfig.aspectRatio.toFixed(2)}:1`;\n parts.push(\n `Image will be cropped to a ${ratioDisplay} rect (${cropConfig.outputWidth}x${cropConfig.outputHeight}px).`,\n );\n }\n\n const sizeParts = acceptTypes\n .map((t) =>\n maxSizeMB[t] ? `${t.charAt(0).toUpperCase() + t.slice(1)} (${maxSizeMB[t]}MB)` : null,\n )\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(\n (e: FileInputError, type: SimplifiedMediaType = 'other') => {\n setLocalError(e);\n onError?.(e);\n onChange({ type, error: e });\n },\n [onError, onChange],\n );\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(\n 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(\n err('invalid_type', `Invalid file type. Allowed: ${acceptTypes.join(', ')}.`),\n fileType,\n );\n return;\n }\n\n const maxBytes = (maxSizeMB[fileType] ?? Infinity) * 1024 * 1024;\n if (file.size > maxBytes) {\n emitError(\n err('too_large', `File too large. Max ${maxSizeMB[fileType]}MB for ${fileType}.`, {\n size: file.size,\n maxBytes,\n }),\n fileType,\n );\n return;\n }\n\n if (fileType === 'video' && videoMaxDurationSec) {\n const ok = await validateMediaDuration(file, videoMaxDurationSec);\n if (!ok) {\n emitError(\n err('too_long', `Video too long. Max ${videoMaxDurationSec} seconds.`),\n fileType,\n );\n return;\n }\n }\n\n if (fileType === 'audio' && audioMaxDurationSec) {\n const ok = await validateMediaDuration(file, audioMaxDurationSec);\n if (!ok) {\n emitError(\n err('too_long', `Audio too long. Max ${audioMaxDurationSec} seconds.`),\n fileType,\n );\n return;\n }\n }\n\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 setInternalSelected(file);\n const previewUrl = makePreviewUrl(file);\n onChange({ type: fileType, file, previewUrl, revokePreviewUrl: revokeLastPreviewUrl });\n },\n [\n acceptTypes,\n maxSizeMB,\n cropConfig,\n videoMaxDurationSec,\n audioMaxDurationSec,\n emitError,\n makePreviewUrl,\n onChange,\n revokeLastPreviewUrl,\n ],\n );\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', {\n 'icon-xl p-0': size === 'icon',\n })}\n >\n <span className=\"relative w-full h-full center-row\">\n {isUploading && (\n <Progress\n value={uploadProgress ?? 0}\n className=\"absolute left-0 top-0 h-full w-full z-0 bg-accent\"\n />\n )}\n\n <span className=\"z-10 center-row w-full\">\n {isLoading ? (\n <>\n <Spinner size=\"xs\" className=\"mr-2\" />{' '}\n {isUploading ? `${(uploadProgress ?? 0).toFixed(0)}%` : 'Processing...'}\n </>\n ) : fileValue ? (\n <span className=\"flex items-center justify-between w-full\">\n <span className=\"truncate pr-2\">{getSelectedFileLabel(fileValue)}</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"icon-sm shrink-0\"\n onClick={handleClear}\n aria-label={clearLabel}\n >\n <X className=\"icon-xs\" />\n </Button>\n </span>\n ) : (\n <>\n {buttonLabel === 'Attach' ? (\n <>\n <Paperclip className=\"mr-2 icon-xs\" /> {buttonLabel}\n </>\n ) : (\n <>\n {getUploadIcon()} {buttonLabel}\n </>\n )}\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\">\n {part}\n </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;AAerD,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE5E,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,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,cAAc,EAAE,CAAC,WAAW,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;CACpD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,4BAiG9D"}
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;AAerD,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE5E,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,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,cAAc,EAAE,CAAC,WAAW,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;CACpD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,4BA+F9D"}
@@ -13,8 +13,6 @@ export function ImageCropperModal(props) {
13
13
  const handleCropComplete = useCallback((_a, pixels) => {
14
14
  setCroppedAreaPixels(pixels);
15
15
  }, []);
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
16
  const confirm = useAsyncAction(async () => {
19
17
  if (!imageSrc || !croppedAreaPixels)
20
18
  return;
@@ -1 +1 @@
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 {\n Button,\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogFooter,\n DialogDescription,\n Slider,\n useAsyncAction,\n} from '@ttt-productions/ui-core/react';\nimport { getCroppedImg, type CropEncoding } 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 encoding?: CropEncoding;\n onCropComplete: (croppedBlob: Blob | null) => void;\n}\n\nexport function ImageCropperModal(props: ImageCropperModalProps) {\n const {\n isOpen,\n onClose,\n imageSrc,\n aspectRatio,\n shape,\n outputWidth,\n outputHeight,\n encoding,\n onCropComplete,\n } = 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(\n imageSrc,\n croppedAreaPixels,\n 0,\n { horizontal: false, vertical: false },\n outputWidth,\n outputHeight,\n encoding,\n );\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>\n Adjust the crop area and zoom level to select the desired portion of your image.\n </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\">\n Zoom\n </label>\n <Slider\n id=\"zoom\"\n min={1}\n max={3}\n step={0.1}\n value={[zoom]}\n onValueChange={(v) => setZoom(v[0])}\n />\n </div>\n\n <DialogFooter className=\"flex-row justify-end gap-2\">\n <Button type=\"button\" variant=\"outline\" onClick={onClose} disabled={confirm.pending}>\n Cancel\n </Button>\n <Button\n type=\"button\"\n variant=\"default\"\n onClick={() => void confirm.run()}\n pending={confirm.pending}\n >\n Confirm\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n}\n"]}
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 {\n Button,\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogFooter,\n DialogDescription,\n Slider,\n useAsyncAction,\n} from '@ttt-productions/ui-core/react';\nimport { getCroppedImg, type CropEncoding } 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 encoding?: CropEncoding;\n onCropComplete: (croppedBlob: Blob | null) => void;\n}\n\nexport function ImageCropperModal(props: ImageCropperModalProps) {\n const {\n isOpen,\n onClose,\n imageSrc,\n aspectRatio,\n shape,\n outputWidth,\n outputHeight,\n encoding,\n onCropComplete,\n } = 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 const confirm = useAsyncAction(\n async () => {\n if (!imageSrc || !croppedAreaPixels) return;\n const blob = await getCroppedImg(\n imageSrc,\n croppedAreaPixels,\n 0,\n { horizontal: false, vertical: false },\n outputWidth,\n outputHeight,\n encoding,\n );\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>\n Adjust the crop area and zoom level to select the desired portion of your image.\n </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\">\n Zoom\n </label>\n <Slider\n id=\"zoom\"\n min={1}\n max={3}\n step={0.1}\n value={[zoom]}\n onValueChange={(v) => setZoom(v[0])}\n />\n </div>\n\n <DialogFooter className=\"flex-row justify-end gap-2\">\n <Button type=\"button\" variant=\"outline\" onClick={onClose} disabled={confirm.pending}>\n Cancel\n </Button>\n <Button\n type=\"button\"\n variant=\"default\"\n onClick={() => void confirm.run()}\n pending={confirm.pending}\n >\n Confirm\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"media-constraints-hint.d.ts","sourceRoot":"","sources":["../../../src/react/components/media-constraints-hint.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAItE,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,eAAe,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA0BD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,yBAAyB,sCA4EpE"}
1
+ {"version":3,"file":"media-constraints-hint.d.ts","sourceRoot":"","sources":["../../../src/react/components/media-constraints-hint.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAItE,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,eAAe,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAyBD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,yBAAyB,sCA4EpE"}
@@ -14,7 +14,6 @@ function fmtBytes(bytes) {
14
14
  function fmtAspect(x) {
15
15
  if (!x)
16
16
  return null;
17
- // show common approx ratios if close
18
17
  const candidates = [
19
18
  ['1:1', 1],
20
19
  ['4:5', 4 / 5],