@feugene/granularity 0.34.2 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/chunks/{config-_QF0O1vq.js → config-CMNfZO7S.js} +2 -2
  2. package/dist/chunks/config-CMNfZO7S.js.map +1 -0
  3. package/dist/chunks/{config-mlheIMtV.js → config-DBi0GVce.js} +2 -2
  4. package/dist/chunks/config-DBi0GVce.js.map +1 -0
  5. package/dist/chunks/{grModalStyles-EZfuEiw_.js → grModalStyles-BEkiFal_.js} +10 -1
  6. package/dist/chunks/grModalStyles-BEkiFal_.js.map +1 -0
  7. package/dist/chunks/{granular-provider-BAy4APOX.js → granular-provider-Bp_BCBnr.js} +2 -2
  8. package/dist/chunks/{granular-provider-BAy4APOX.js.map → granular-provider-Bp_BCBnr.js.map} +1 -1
  9. package/dist/components/GrColorPicker/chunks/{GrColorPicker-CIgdPXHn.js → GrColorPicker-B-L4CWMt.js} +2 -2
  10. package/dist/components/GrColorPicker/chunks/{GrColorPicker-CIgdPXHn.js.map → GrColorPicker-B-L4CWMt.js.map} +1 -1
  11. package/dist/components/GrColorPicker/index.js +1 -1
  12. package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-BZHUKlux.js → GrCommandPalette-8HkYmMVP.js} +1 -1
  13. package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-BZHUKlux.js.map → GrCommandPalette-8HkYmMVP.js.map} +1 -1
  14. package/dist/components/GrCommandPalette/index.js +1 -1
  15. package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-jRDs_E6C.js → GrConfirmDialog-CZEO2JR4.js} +1 -1
  16. package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-jRDs_E6C.js.map → GrConfirmDialog-CZEO2JR4.js.map} +1 -1
  17. package/dist/components/GrConfirmDialog/index.js +1 -1
  18. package/dist/components/GrDialog/chunks/{GrDialog-C01Hhk6G.js → GrDialog-CKNbnxwO.js} +1 -1
  19. package/dist/components/GrDialog/chunks/{GrDialog-C01Hhk6G.js.map → GrDialog-CKNbnxwO.js.map} +1 -1
  20. package/dist/components/GrDialog/index.js +1 -1
  21. package/dist/components/GrDialogService/chunks/{GrDialogService-CmsJNUmM.js → GrDialogService-Ut8iiBQh.js} +2 -2
  22. package/dist/components/GrDialogService/chunks/{GrDialogService-CmsJNUmM.js.map → GrDialogService-Ut8iiBQh.js.map} +1 -1
  23. package/dist/components/GrDialogService/index.js +1 -1
  24. package/dist/components/GrFileUpload/chunks/{GrFileUpload-LL8RZvC0.js → GrFileUpload-C0ROpmOY.js} +1 -1
  25. package/dist/components/GrFileUpload/chunks/{GrFileUpload-LL8RZvC0.js.map → GrFileUpload-C0ROpmOY.js.map} +1 -1
  26. package/dist/components/GrFileUpload/index.js +1 -1
  27. package/dist/components/GrInput/chunks/{GrInput-BLywRZUN.js → GrInput-CJ7eu3fR.js} +1 -0
  28. package/dist/components/GrInput/chunks/{GrInput-BLywRZUN.js.map → GrInput-CJ7eu3fR.js.map} +1 -1
  29. package/dist/components/GrInput/index.js +1 -1
  30. package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-BCZ7lE2k.js → GrJsonViewer-BXG7DdTe.js} +1 -1
  31. package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-BCZ7lE2k.js.map → GrJsonViewer-BXG7DdTe.js.map} +1 -1
  32. package/dist/components/GrJsonViewer/index.js +1 -1
  33. package/dist/components/GrModal/chunks/{GrModal-_JrDhon-.js → GrModal-DixnhJcu.js} +3 -2
  34. package/dist/components/GrModal/chunks/GrModal-DixnhJcu.js.map +1 -0
  35. package/dist/components/GrModal/index.js +2 -2
  36. package/dist/components/GrNavbar/chunks/{GrNavbar-_rtHKwTf.js → GrNavbar-Dr8e5kW-.js} +14 -4
  37. package/dist/components/GrNavbar/chunks/GrNavbar-Dr8e5kW-.js.map +1 -0
  38. package/dist/components/GrNavbar/index.js +1 -1
  39. package/dist/components/GrPagination/chunks/{GrPagination-gBoWVHk6.js → GrPagination-68iyAwQz.js} +24 -9
  40. package/dist/components/GrPagination/chunks/GrPagination-68iyAwQz.js.map +1 -0
  41. package/dist/components/GrPagination/index.js +1 -1
  42. package/dist/components/GrProgressBar/chunks/{GrProgressBar-hszUOC8f.js → GrProgressBar-Cd7AaJtR.js} +5 -2
  43. package/dist/components/GrProgressBar/chunks/GrProgressBar-Cd7AaJtR.js.map +1 -0
  44. package/dist/components/GrProgressBar/index.js +1 -1
  45. package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-CailXSqW.js → GrPromptDialog-Ckeombia.js} +2 -2
  46. package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-CailXSqW.js.map → GrPromptDialog-Ckeombia.js.map} +1 -1
  47. package/dist/components/GrPromptDialog/index.js +1 -1
  48. package/dist/components/GrSelect/chunks/{GrSelect-CZqV1Eg4.js → GrSelect-Bm3Jugx8.js} +3 -7
  49. package/dist/components/GrSelect/chunks/GrSelect-Bm3Jugx8.js.map +1 -0
  50. package/dist/components/GrSelect/index.js +1 -1
  51. package/dist/components/GrSelect/styles.css +1 -1
  52. package/dist/components/GrSlider/chunks/{GrSlider-DTTrZTHK.js → GrSlider-DM6W6U9c.js} +9 -3
  53. package/dist/components/GrSlider/chunks/GrSlider-DM6W6U9c.js.map +1 -0
  54. package/dist/components/GrSlider/index.js +1 -1
  55. package/dist/components/GrStatistic/chunks/{GrStatistic-qHVasK37.js → GrStatistic-BoLkXw7F.js} +7 -3
  56. package/dist/components/GrStatistic/chunks/GrStatistic-BoLkXw7F.js.map +1 -0
  57. package/dist/components/GrStatistic/index.js +1 -1
  58. package/dist/components/GrSteps/chunks/{GrSteps-C2jxpZoN.js → GrSteps-CpGHXLOc.js} +1 -1
  59. package/dist/components/GrSteps/chunks/{GrSteps-C2jxpZoN.js.map → GrSteps-CpGHXLOc.js.map} +1 -1
  60. package/dist/components/GrSteps/index.js +1 -1
  61. package/dist/components/GrTabs/chunks/{GrTabs-CEKT5tXv.js → GrTabs-CT2nk_HV.js} +1 -1
  62. package/dist/components/GrTabs/chunks/{GrTabs-CEKT5tXv.js.map → GrTabs-CT2nk_HV.js.map} +1 -1
  63. package/dist/components/GrTabs/index.js +2 -2
  64. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-DOXcpWq2.js → GrTreeSelect-MGsyvkJV.js} +1 -1
  65. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-DOXcpWq2.js.map → GrTreeSelect-MGsyvkJV.js.map} +1 -1
  66. package/dist/components/GrTreeSelect/index.js +1 -1
  67. package/dist/granular-provider-node.js +1 -1
  68. package/dist/granular-provider.js +1 -1
  69. package/dist/index.js +21 -21
  70. package/dist/types/components/GrModal/grModalStyles.d.ts +9 -0
  71. package/dist/types/components/GrNavbar/GrNavbar.vue.d.ts +14 -0
  72. package/dist/types/components/GrNavbar/defaults.d.ts +10 -0
  73. package/dist/types/components/GrNavbar/index.d.ts +1 -0
  74. package/dist/types/components/GrProgressBar/GrProgressBar.vue.d.ts +1 -1
  75. package/dist/web-types.json +11 -2
  76. package/package.json +3 -3
  77. package/dist/chunks/config-_QF0O1vq.js.map +0 -1
  78. package/dist/chunks/config-mlheIMtV.js.map +0 -1
  79. package/dist/chunks/grModalStyles-EZfuEiw_.js.map +0 -1
  80. package/dist/components/GrModal/chunks/GrModal-_JrDhon-.js.map +0 -1
  81. package/dist/components/GrNavbar/chunks/GrNavbar-_rtHKwTf.js.map +0 -1
  82. package/dist/components/GrPagination/chunks/GrPagination-gBoWVHk6.js.map +0 -1
  83. package/dist/components/GrProgressBar/chunks/GrProgressBar-hszUOC8f.js.map +0 -1
  84. package/dist/components/GrSelect/chunks/GrSelect-CZqV1Eg4.js.map +0 -1
  85. package/dist/components/GrSlider/chunks/GrSlider-DTTrZTHK.js.map +0 -1
  86. package/dist/components/GrStatistic/chunks/GrStatistic-qHVasK37.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"GrFileUpload-LL8RZvC0.js","names":["$slots"],"sources":["../../../../src/components/GrFileUpload/fileEntry.ts","../../../../src/components/GrFileUpload/useDropZone.ts","../../../../src/components/GrFileUpload/uploadViaXhr.ts","../../../../src/components/GrFileUpload/usePerFileUpload.ts","../../../../src/components/GrFileUpload/uploadState.ts","../../../../src/components/GrFileUpload/useUploadState.ts","../../../../src/components/GrFileUpload/GrFileUpload.vue","../../../../src/components/GrFileUpload/GrFileUpload.vue"],"sourcesContent":["import type { GrUploadState } from './uploadState'\n\n/**\n * Состояние одного файла в пофайловом режиме (`uploadMode=\"per-file\"`).\n *\n * Батчевый режим по-прежнему знает только сводное `GrUploadState`: весь набор\n * уходит одним запросом, и говорить про отдельный файл там нечем.\n */\nexport type GrFileUploadStatus = 'pending' | 'uploading' | 'success' | 'error'\n\nexport interface GrFileUploadEntry {\n file: File\n status: GrFileUploadStatus\n /** Процент `0..100`. У `pending` — 0, у `success` — 100. */\n percent: number\n /** Причина ошибки, если `status === 'error'`. */\n error?: unknown\n}\n\nexport function createFileEntry(file: File): GrFileUploadEntry {\n return { file, status: 'pending', percent: 0 }\n}\n\n/**\n * Сводное состояние набора: грузимся, пока грузится хоть один; ошибка, если\n * после завершения есть хоть одна; успех — когда успешны все.\n *\n * Процент — среднее по файлам, а не по байтам: у пофайловых запросов размеры\n * разные, и «взвешенная» полоса дёргалась бы назад, когда большой файл\n * стартует после маленького.\n */\nexport function summarizeFileEntries(entries: GrFileUploadEntry[]): GrUploadState {\n if (!entries.length)\n return { phase: 'idle', percent: 0, indeterminate: false, loaded: 0, total: 0 }\n\n const total = entries.length\n const percent = Math.round(entries.reduce((sum, entry) => sum + entry.percent, 0) / total)\n const failed = entries.filter(entry => entry.status === 'error')\n const active = entries.some(entry => entry.status === 'uploading' || entry.status === 'pending')\n\n if (active) {\n return {\n phase: 'uploading',\n percent,\n // Неопределённым считаем только самое начало: пока ни один файл не\n // отчитался, полоса не должна притворяться, что что-то знает.\n indeterminate: percent === 0,\n loaded: 0,\n total: 0,\n }\n }\n\n if (failed.length) {\n return {\n phase: 'error',\n percent,\n indeterminate: false,\n loaded: 0,\n total: 0,\n error: failed[0].error,\n }\n }\n\n return { phase: 'success', percent: 100, indeterminate: false, loaded: 0, total: 0 }\n}\n","import { ref, type Ref } from 'vue'\n\nexport interface UseDropZoneOptions {\n /** Зона выключена: ввод не принимается (`disabled` или `readonly`). */\n locked: () => boolean\n /** UI рисует слот — зона перестаёт быть зоной, обработка уезжает к нему. */\n inactive: () => boolean\n onDrop: (files: File[]) => void\n}\n\nexport interface UseDropZoneReturn {\n /** Файл висит над зоной. */\n isOver: Ref<boolean>\n onDragEnter: (event: DragEvent) => void\n onDragOver: (event: DragEvent) => void\n onDragLeave: (event: DragEvent) => void\n onDropFiles: (event: DragEvent) => void\n}\n\n/**\n * Drag&drop поверх зоны.\n *\n * Счётчик входов, а не флаг: `dragenter`/`dragleave` приходят и от вложенных\n * элементов, и без счётчика подсветка гасла, стоило курсору пересечь границу\n * подписи внутри зоны.\n */\nexport function useDropZone(options: UseDropZoneOptions): UseDropZoneReturn {\n const isOver = ref(false)\n let overCounter = 0\n\n function setOver(next: boolean): void {\n if (isOver.value === next)\n return\n isOver.value = next\n }\n\n function ignored(): boolean {\n return options.inactive() || options.locked()\n }\n\n function onDragEnter(event: DragEvent): void {\n if (ignored())\n return\n\n event.preventDefault()\n overCounter += 1\n setOver(true)\n }\n\n function onDragOver(event: DragEvent): void {\n if (ignored())\n return\n\n event.preventDefault()\n if (event.dataTransfer)\n event.dataTransfer.dropEffect = 'copy'\n }\n\n function onDragLeave(event: DragEvent): void {\n if (ignored())\n return\n\n event.preventDefault()\n overCounter = Math.max(0, overCounter - 1)\n if (overCounter === 0)\n setOver(false)\n }\n\n function onDropFiles(event: DragEvent): void {\n if (ignored())\n return\n\n event.preventDefault()\n overCounter = 0\n setOver(false)\n\n const files = event.dataTransfer?.files\n ? Array.prototype.slice.call(event.dataTransfer.files) as File[]\n : []\n\n options.onDrop(files)\n }\n\n return { isOver, onDragEnter, onDragOver, onDragLeave, onDropFiles }\n}\n","/**\n * Загрузка файлов через `XMLHttpRequest` с поддержкой прогресса аплоада и\n * отмены по `AbortSignal`. Используется в `GrFileUpload` для ветки `action`,\n * а также может применяться пользователем напрямую в `request`-загрузчике.\n */\n\nexport type GrUploadExtraDataValue = string | Blob\n\nexport type GrUploadExtraData = Record<string, GrUploadExtraDataValue | GrUploadExtraDataValue[]>\n\nexport interface GrUploadProgressInfo {\n /** Процент `0..100`. Для запросов без `lengthComputable` значение `0` и `indeterminate = true`. */\n percent: number\n loaded: number\n total: number\n indeterminate: boolean\n}\n\nexport interface UploadViaXhrOptions {\n url: string\n files: File[]\n name: string\n headers?: Record<string, string>\n withCredentials?: boolean\n extraData?: GrUploadExtraData\n signal: AbortSignal\n onProgress?: (info: GrUploadProgressInfo) => void\n}\n\nexport class GrUploadAbortError extends Error {\n constructor(message = 'Upload aborted') {\n super(message)\n this.name = 'GrUploadAbortError'\n }\n}\n\nexport class GrUploadHttpError extends Error {\n status: number\n payload: unknown\n\n constructor(status: number, payload: unknown) {\n super(`Upload failed with status ${status}`)\n this.name = 'GrUploadHttpError'\n this.status = status\n this.payload = payload\n }\n}\n\nfunction appendExtraFormData(body: FormData, extraData: GrUploadExtraData | undefined) {\n if (!extraData)\n return\n\n for (const [key, value] of Object.entries(extraData)) {\n if (Array.isArray(value)) {\n for (const entry of value) body.append(key, entry)\n continue\n }\n\n body.append(key, value)\n }\n}\n\nfunction parseResponse(xhr: XMLHttpRequest): unknown {\n const contentType = xhr.getResponseHeader('content-type') ?? ''\n const raw = xhr.responseText\n\n if (contentType.includes('application/json')) {\n try {\n return JSON.parse(raw)\n }\n catch {\n return raw\n }\n }\n\n return raw\n}\n\nexport function uploadViaXhr(options: UploadViaXhrOptions): Promise<unknown> {\n const { url, files, name, headers, withCredentials, extraData, signal, onProgress } = options\n\n return new Promise<unknown>((resolve, reject) => {\n if (signal.aborted) {\n reject(new GrUploadAbortError())\n return\n }\n\n const body = new FormData()\n for (const file of files) body.append(name, file)\n appendExtraFormData(body, extraData)\n\n const xhr = new XMLHttpRequest()\n xhr.open('POST', url, true)\n xhr.withCredentials = !!withCredentials\n\n if (headers) {\n for (const [key, value] of Object.entries(headers))\n xhr.setRequestHeader(key, value)\n }\n\n const onAbort = () => {\n try {\n xhr.abort()\n }\n catch {\n /* ignore */\n }\n }\n\n signal.addEventListener('abort', onAbort)\n\n const cleanup = () => {\n signal.removeEventListener('abort', onAbort)\n }\n\n if (onProgress) {\n xhr.upload.addEventListener('progress', (event) => {\n const indeterminate = !event.lengthComputable\n const total = indeterminate ? 0 : event.total\n const loaded = indeterminate ? 0 : event.loaded\n const percent = indeterminate || total === 0 ? 0 : Math.min(100, (loaded / total) * 100)\n onProgress({ percent, loaded, total, indeterminate })\n })\n }\n\n xhr.addEventListener('load', () => {\n cleanup()\n const payload = parseResponse(xhr)\n const status = xhr.status\n\n if (status >= 200 && status < 300) {\n if (onProgress)\n onProgress({ percent: 100, loaded: 1, total: 1, indeterminate: false })\n resolve(payload)\n return\n }\n\n reject(new GrUploadHttpError(status, payload))\n })\n\n xhr.addEventListener('error', () => {\n cleanup()\n reject(new Error('Network error during upload'))\n })\n\n xhr.addEventListener('abort', () => {\n cleanup()\n reject(new GrUploadAbortError())\n })\n\n xhr.send(body)\n })\n}\n","import { ref, type Ref } from 'vue'\n\nimport { createFileEntry, summarizeFileEntries, type GrFileUploadEntry } from './fileEntry'\nimport type { GrUploadState } from './uploadState'\nimport { GrUploadAbortError, type GrUploadProgressInfo } from './uploadViaXhr'\n\nexport interface UsePerFileUploadOptions {\n /** Сколько файлов грузится одновременно. */\n concurrency: () => number\n /** Отправка одного файла. Контракт тот же, что у батча: всегда массив. */\n send: (files: File[], ctx: { signal: AbortSignal, onProgress: (info: GrUploadProgressInfo) => void }) => Promise<unknown>\n /** Сводное состояние пересчитывается из записей по файлам. */\n onSummary: (state: GrUploadState) => void\n onProgress: (info: GrUploadProgressInfo, file: File) => void\n onSuccess: (payload: unknown, file: File) => void\n onError: (error: unknown, file: File) => void\n onSetChange: (files: File[]) => void\n}\n\nexport interface UsePerFileUploadReturn {\n entries: Ref<GrFileUploadEntry[]>\n entryOf: (file: File) => GrFileUploadEntry | undefined\n /** Загрузить набор пофайлово: у каждого файла свой запрос и свой статус. */\n run: (files: File[]) => Promise<void>\n /** Повторить один файл — соседей это не касается. */\n retryFile: (file: File) => Promise<void>\n /** Оборвать загрузку одного файла. */\n abortFile: (file: File) => void\n abortAll: () => void\n /** Убрать файл из набора записей и пересчитать сводное состояние. */\n dropEntry: (file: File) => void\n reset: () => void\n}\n\n/**\n * Пофайловый режим: у каждого файла свой запрос, свой контроллер и свой статус,\n * а сводное состояние выводится из них.\n */\nexport function usePerFileUpload(options: UsePerFileUploadOptions): UsePerFileUploadReturn {\n const entries = ref<GrFileUploadEntry[]>([])\n\n /** Контроллер на файл: `abortFile` обязан обрывать свой запрос, а не соседний. */\n const controllers = new Map<File, AbortController>()\n\n function syncSummary(): void {\n options.onSummary(summarizeFileEntries(entries.value))\n }\n\n function entryOf(file: File): GrFileUploadEntry | undefined {\n return entries.value.find(entry => entry.file === file)\n }\n\n function patch(file: File, next: Partial<GrFileUploadEntry>): void {\n const entry = entryOf(file)\n if (!entry)\n return\n\n Object.assign(entry, next)\n syncSummary()\n }\n\n async function uploadOne(file: File): Promise<void> {\n const controller = new AbortController()\n controllers.set(file, controller)\n patch(file, { status: 'uploading', percent: 0, error: undefined })\n\n const onProgress = (info: GrUploadProgressInfo): void => {\n patch(file, { percent: info.indeterminate ? 0 : info.percent })\n options.onProgress(info, file)\n }\n\n try {\n const payload = await options.send([file], { signal: controller.signal, onProgress })\n\n patch(file, { status: 'success', percent: 100, error: undefined })\n options.onSuccess(payload, file)\n options.onProgress({ percent: 100, loaded: 0, total: 0, indeterminate: false }, file)\n }\n catch (error) {\n // Отмена — не ошибка файла: строка возвращается в исходное состояние, а\n // пользователь решает, повторять её или убрать.\n if (error instanceof GrUploadAbortError || (error as { name?: string })?.name === 'AbortError')\n patch(file, { status: 'pending', percent: 0, error: undefined })\n else\n patch(file, { status: 'error', error })\n\n options.onError(error, file)\n }\n finally {\n controllers.delete(file)\n }\n }\n\n /**\n * Очередь с ограничением параллелизма. Без неё «пофайлово» означало бы «все\n * сразу»: сотня файлов открыла бы сотню соединений.\n */\n async function runWithConcurrency(files: File[]): Promise<void> {\n const limit = Math.max(1, Math.floor(options.concurrency()))\n const queue = [...files]\n\n const workers = Array.from({ length: Math.min(limit, queue.length) }, async () => {\n while (queue.length) {\n const next = queue.shift()\n if (!next)\n return\n await uploadOne(next)\n }\n })\n\n await Promise.all(workers)\n }\n\n async function run(files: File[]): Promise<void> {\n entries.value = files.map(createFileEntry)\n syncSummary()\n\n await runWithConcurrency(files)\n options.onSetChange(files)\n }\n\n async function retryFile(file: File): Promise<void> {\n if (!entryOf(file))\n return\n\n await uploadOne(file)\n }\n\n function abortFile(file: File): void {\n controllers.get(file)?.abort()\n controllers.delete(file)\n }\n\n function abortAll(): void {\n for (const controller of controllers.values()) controller.abort()\n controllers.clear()\n }\n\n function dropEntry(file: File): void {\n entries.value = entries.value.filter(entry => entry.file !== file)\n }\n\n function reset(): void {\n entries.value = []\n }\n\n return { entries, entryOf, run, retryFile, abortFile, abortAll, dropEntry, reset }\n}\n","/**\n * Состояние процесса загрузки в `GrFileUpload`.\n *\n * Используется как для внутреннего управления UI, так и пробрасывается наружу\n * через scoped-слоты (`progress`, `default`) и `defineExpose`.\n */\n\nexport type GrUploadPhase = 'idle' | 'uploading' | 'success' | 'error'\n\nexport interface GrUploadStateIdle {\n phase: 'idle'\n percent: 0\n indeterminate: false\n loaded: 0\n total: 0\n}\n\nexport interface GrUploadStateUploading {\n phase: 'uploading'\n percent: number\n indeterminate: boolean\n loaded: number\n total: number\n}\n\nexport interface GrUploadStateSuccess {\n phase: 'success'\n percent: 100\n indeterminate: false\n loaded: number\n total: number\n}\n\nexport interface GrUploadStateError {\n phase: 'error'\n percent: number\n indeterminate: false\n loaded: number\n total: number\n error: unknown\n}\n\nexport type GrUploadState\n = | GrUploadStateIdle\n | GrUploadStateUploading\n | GrUploadStateSuccess\n | GrUploadStateError\n\nexport const GR_UPLOAD_STATE_IDLE: GrUploadStateIdle = Object.freeze({\n phase: 'idle',\n percent: 0,\n indeterminate: false,\n loaded: 0,\n total: 0,\n})\n","import { reactive } from 'vue'\n\nimport { GR_UPLOAD_STATE_IDLE, type GrUploadState } from './uploadState'\nimport type { GrUploadProgressInfo } from './uploadViaXhr'\n\nexport interface UseUploadStateOptions {\n /** Компонент отчитывается о каждой смене фазы наружу. */\n onChange: (state: GrUploadState) => void\n}\n\nexport interface UseUploadStateReturn {\n state: GrUploadState\n setIdle: () => void\n setUploading: (info?: GrUploadProgressInfo) => void\n setSuccess: (info: { loaded: number, total: number }) => void\n setError: (error: unknown) => void\n /** Прогресс не двигает состояние вне фазы `uploading`. */\n applyProgress: (info: GrUploadProgressInfo) => void\n assign: (next: GrUploadState) => void\n /** Отложенный переход `success` → `idle`. */\n scheduleIdle: (delayMs: number) => void\n clearScheduledIdle: () => void\n}\n\n/**\n * Сводное состояние загрузки: фаза, проценты и таймер скрытия успеха.\n *\n * Состояние — `reactive`-объект с меняющейся формой (у `error` есть поле\n * `error`, у `idle` его нет), поэтому при смене фазы старые ключи удаляются:\n * иначе `phase: 'idle'` тащил бы за собой `error` от прошлой попытки.\n */\nexport function useUploadState(options: UseUploadStateOptions): UseUploadStateReturn {\n const state = reactive<GrUploadState>({ ...GR_UPLOAD_STATE_IDLE }) as GrUploadState\n\n let idleTimer: ReturnType<typeof setTimeout> | null = null\n\n function clearScheduledIdle(): void {\n if (idleTimer === null)\n return\n\n clearTimeout(idleTimer)\n idleTimer = null\n }\n\n function assign(next: GrUploadState): void {\n // Форма состояния меняется вместе с фазой (`error` есть только у ошибки),\n // поэтому старые ключи снимаются: иначе `idle` тащил бы `error` прошлой\n // попытки.\n for (const key of Object.keys(state))\n delete (state as unknown as Record<string, unknown>)[key]\n\n Object.assign(state, next)\n options.onChange(state)\n }\n\n function setIdle(): void {\n assign({ ...GR_UPLOAD_STATE_IDLE })\n }\n\n function setUploading(info?: GrUploadProgressInfo): void {\n assign({\n phase: 'uploading',\n percent: info?.percent ?? 0,\n indeterminate: info?.indeterminate ?? true,\n loaded: info?.loaded ?? 0,\n total: info?.total ?? 0,\n })\n }\n\n function setSuccess(info: { loaded: number, total: number }): void {\n assign({\n phase: 'success',\n percent: 100,\n indeterminate: false,\n loaded: info.loaded,\n total: info.total,\n })\n }\n\n function setError(error: unknown): void {\n const prev = state\n assign({\n phase: 'error',\n percent: prev.phase === 'uploading' ? prev.percent : 0,\n indeterminate: false,\n loaded: prev.phase === 'uploading' ? prev.loaded : 0,\n total: prev.phase === 'uploading' ? prev.total : 0,\n error,\n })\n }\n\n function applyProgress(info: GrUploadProgressInfo): void {\n if (state.phase !== 'uploading')\n return\n\n assign({\n phase: 'uploading',\n percent: info.percent,\n indeterminate: info.indeterminate,\n loaded: info.loaded,\n total: info.total,\n })\n }\n\n function scheduleIdle(delayMs: number): void {\n clearScheduledIdle()\n idleTimer = setTimeout(() => {\n if (state.phase === 'success')\n setIdle()\n idleTimer = null\n }, delayMs)\n }\n\n return {\n state,\n setIdle,\n setUploading,\n setSuccess,\n setError,\n applyProgress,\n assign,\n scheduleIdle,\n clearScheduledIdle,\n }\n}\n","<script setup lang=\"ts\" generic=\"TResponse = unknown\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, Text, useSlots, watch, type VNode } from 'vue'\n\nimport IconArrowUp from '~icons/lucide/arrow-up'\nimport IconClose from '~icons/lucide/x'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport GrProgressBar from '../GrProgressBar/GrProgressBar.vue'\nimport type { GrProgressBarTone } from '../GrProgressBar/grStyle'\nimport type { FileValidator, FileValidatorSource } from '../../fileValidation'\nimport type { GrUploadProgressInfo } from './uploadViaXhr'\nimport type { GrUploadState } from './uploadState'\nimport { summarizeFileEntries, type GrFileUploadEntry } from './fileEntry'\nimport { useDropZone } from './useDropZone'\nimport { useFilePreviews } from '../../composables/internal/useFilePreviews'\nimport { usePerFileUpload } from './usePerFileUpload'\nimport { useUploadState } from './useUploadState'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { flattenSlotNodes, meaningfulSlotNodes } from '../shared/slotNodes'\nimport {\n type GrFileUploadSize,\n grFileUploadZoneClass,\n hintSizes,\n iconGlyphSizes,\n iconTileSizes,\n labelSizes,\n progressBarSizes,\n progressTextSizes,\n zoneGaps,\n} from './grFileUploadStyles'\nimport { acceptValidator, FileValidationError, runFileValidators } from '../../fileValidation'\nimport { GrUploadAbortError, uploadViaXhr } from './uploadViaXhr'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nexport type GrFileUploadMode = 'batch' | 'per-file'\n\nexport type GrFileUploadExtraDataValue = string | Blob\n\nexport type GrFileUploadExtraData = Record<string, GrFileUploadExtraDataValue | GrFileUploadExtraDataValue[]>\n\nexport type GrFileUploadRequestCtx = {\n signal: AbortSignal\n extraData?: GrFileUploadExtraData\n /**\n * Колбэк прогресса аплоада. Пользовательский `request` (например, axios)\n * должен вызывать его из `onUploadProgress`, чтобы `GrFileUpload` отображал\n * прогресс через слот `progress` / дефолтный `GrProgressBar`.\n */\n onProgress?: (info: GrUploadProgressInfo) => void\n}\n\nexport type GrFileUploadRequest<TResponse = unknown> = (\n files: File[],\n ctx: GrFileUploadRequestCtx,\n) => Promise<TResponse>\n\n/**\n * Пропы `GrFileUpload`.\n *\n * Либо `action` (URL для POST multipart/form-data), либо `request` — кастомный\n * загрузчик (например, через axios). Если переданы оба — приоритет у `request`.\n *\n * `placeholder` — надпись в дефолтном UI-варианте (без слота default).\n */\nexport interface GrFileUploadProps<TResponse = unknown> {\n /**\n * Набор файлов — тот, что показан в списке и уйдёт в `retry`.\n *\n * Проп **необязателен**: без него компонент держит набор сам. Передан —\n * набор следует за ним, и потребитель может очистить или подменить список\n * снаружи.\n */\n modelValue?: File[]\n action?: string\n request?: GrFileUploadRequest<TResponse>\n name?: string\n multiple?: boolean\n limit?: number\n /**\n * Guard перед отправкой. Остаётся пропом, а не эмитом, осознанно: эмит не\n * возвращает значения, а этот колбэк обязан ответить «пускать или нет».\n * Уведомления — `exceed`, `success`, `error`, `progress` — эмиты.\n */\n beforeUpload?: (file: File) => boolean | Promise<unknown>\n validators?: FileValidator[]\n /**\n * W3C `accept` для `<input type=\"file\">` — и sugar к `acceptValidator(...)`.\n *\n * Только атрибута мало: он фильтрует системный диалог, но не drag&drop —\n * перетащить можно что угодно. Поэтому то же значение уходит и в валидаторы,\n * как в `GrFormFile`.\n */\n accept?: string\n /** `capture` для мобильной камеры/микрофона. */\n capture?: 'user' | 'environment'\n /** Выбор каталога целиком (`webkitdirectory`). Поддержка — Chromium и Safari. */\n directory?: boolean\n disabled?: boolean\n /** Только для чтения: значение видно и уходит в форму, но не редактируется. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n headers?: Record<string, string>\n withCredentials?: boolean\n showFileList?: boolean\n uploadExtraData?: (files: File[]) => GrFileUploadExtraData | undefined\n /** i18n: надпись-подсказка в дефолтном UI. */\n placeholder?: string\n /** Показывать дефолтный прогресс-бар (если не используется слот `progress`). */\n showProgress?: boolean\n /** Цветовой тон полосы прогресса в фазе `uploading`. */\n progressTone?: GrProgressBarTone\n /** aria-label для прогресс-бара. */\n progressLabel?: string\n /** Через сколько мс после `success` скрыть прогресс-бар. `0` — не скрывать. */\n hideProgressOnSuccess?: number\n /** Размер дефолтного UI: поля дроп-зоны, плитка иконки, кегль подписей. */\n size?: GrFileUploadSize\n /**\n * Как уходит набор файлов:\n *\n * - `batch` (по умолчанию) — весь набор одним запросом. Про отдельный файл\n * сказать нечего, поэтому и статуса у него нет;\n * - `per-file` — на каждый файл свой запрос (`request` зовётся с массивом из\n * одного файла, контракт не меняется). Появляются статус и процент строки,\n * `retryFile` и `abortFile`.\n */\n uploadMode?: GrFileUploadMode\n /** Сколько файлов грузится одновременно в режиме `per-file`. */\n concurrency?: number\n /** Миниатюры для `image/*` в списке файлов. */\n preview?: boolean\n}\n\nexport interface GrFileUploadEmits<TResponse = unknown> {\n /** Набор файлов сменился: новый выбор или удаление. Не путать с `change`. */\n (e: 'update:modelValue', files: File[]): void\n /** Выбрано больше файлов, чем разрешает `limit`. Загрузка не стартует. */\n (e: 'exceed', files: File[], limit: number): void\n /** `file` приходит только в режиме `per-file`: в батче отчитываться нечем. */\n (e: 'success', payload: TResponse, file?: File): void\n (e: 'error', error: unknown, file?: File): void\n (e: 'progress', percent: number, info?: GrUploadProgressInfo, file?: File): void\n (e: 'change', files: File[]): void\n (e: 'stateChange', state: GrUploadState): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrFileUploadProps<TResponse>>(),\n {\n modelValue: undefined,\n action: undefined,\n request: undefined,\n name: 'file',\n multiple: false,\n limit: undefined,\n beforeUpload: undefined,\n validators: undefined,\n accept: undefined,\n capture: undefined,\n directory: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n headers: undefined,\n withCredentials: false,\n showFileList: false,\n uploadExtraData: undefined,\n placeholder: undefined,\n showProgress: true,\n progressTone: 'primary',\n progressLabel: undefined,\n hideProgressOnSuccess: 800,\n size: undefined,\n uploadMode: 'batch',\n concurrency: 3,\n preview: false,\n },\n)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFileUpload' })\n\nconst zoneGapClass = computed(() => zoneGaps[resolvedSize.value])\nconst iconTileClass = computed(() => iconTileSizes[resolvedSize.value])\nconst iconGlyphSize = computed(() => iconGlyphSizes[resolvedSize.value])\nconst labelClass = computed(() => labelSizes[resolvedSize.value])\nconst hintClass = computed(() => hintSizes[resolvedSize.value])\nconst progressTextClass = computed(() => progressTextSizes[resolvedSize.value])\nconst progressBarSize = computed(() => progressBarSizes[resolvedSize.value])\n\nconst emit = defineEmits<GrFileUploadEmits<TResponse>>()\ndefineSlots<{\n /** Полностью своя зона загрузки: контрол отдаёт наружу всё своё состояние. */\n default?: (props: {\n openDialog: () => void\n abort: () => void\n disabled: boolean\n files: File[]\n isOver: boolean\n state: GrUploadState\n retry: () => Promise<void>\n removeFile: (file: File) => void\n fileEntries: GrFileUploadEntry[]\n retryFile: (file: File) => Promise<void>\n abortFile: (file: File) => void\n }) => any\n /** Заголовок зоны вместо `placeholder`. */\n label?: () => any\n /** Подпись под заголовком: ограничения по типу и размеру. */\n tip?: () => any\n /** Свой индикатор прогресса вместо встроенного. */\n progress?: (props: {\n state: GrUploadState\n percent: number\n indeterminate: boolean\n phase: GrUploadState['phase']\n files: File[]\n abort: () => void\n retry: () => Promise<void>\n fileEntries: GrFileUploadEntry[]\n retryFile: (file: File) => Promise<void>\n abortFile: (file: File) => void\n }) => any\n}>()\n\nconst slots = useSlots()\n\nconst { t } = useGranularityTranslations()\n\n// Контекст `GrFormField`: доступным контролом служит сам нативный\n// `<input type=\"file\">` — он и остаётся целью `<label for>`.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n readonly: isReadonly,\n invalid: isInvalid,\n required: isRequired,\n // `locked` — «ввод не принимается»: `disabled` или `readonly`. Набор в\n // `readonly` виден и уходит в форму, но поменять его нельзя.\n locked: isLocked,\n} = useGrFormControl(() => props)\nconst resolvedPlaceholder = computed(() => props.placeholder ?? t('gr.fileUpload.placeholder', 'Drag files here or click to select'))\nconst resolvedProgressLabel = computed(() => props.progressLabel ?? t('gr.fileUpload.progress', 'Upload progress'))\n\nfunction slotIsTextOnly(nodes: VNode[]): boolean {\n const meaningful = meaningfulSlotNodes(nodes)\n\n if (meaningful.length === 0)\n return true\n\n for (const node of meaningful) {\n if (node.type !== Text)\n return false\n }\n\n return true\n}\n\nconst defaultSlotNodes = computed(() => {\n return slots.default ? flattenSlotNodes(slots.default({} as any)) : []\n})\n\nconst hasCustomUi = computed(() => {\n return !!slots.default && !slotIsTextOnly(defaultSlotNodes.value)\n})\n\nconst inputRef = ref<HTMLInputElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Внутри зоны фокус ходит между нативным input'ом и кнопками списка файлов:\n// без границы каждое перемещение давало бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n\nconst lastFiles = ref<File[]>(props.modelValue ? [...props.modelValue] : [])\n\n/**\n * Набор меняется только здесь: иначе `update:modelValue` рано или поздно\n * разошёлся бы с тем, что показано в списке.\n */\nfunction setFiles(next: File[]): void {\n lastFiles.value = next\n emit('update:modelValue', next)\n}\n\n// Контролируемый режим: проп задан — он и есть источник истины.\nwatch(() => props.modelValue, (next) => {\n if (next === undefined)\n return\n if (next === lastFiles.value)\n return\n\n lastFiles.value = [...next]\n}, { deep: false })\n\nconst {\n state,\n setIdle: setStateIdle,\n setUploading: setStateUploading,\n setSuccess: setStateSuccess,\n setError: setStateError,\n applyProgress,\n assign: assignState,\n scheduleIdle,\n clearScheduledIdle,\n} = useUploadState({ onChange: next => emit('stateChange', next) })\n\nlet activeController: AbortController | null = null\n\nfunction handleProgress(info: GrUploadProgressInfo) {\n applyProgress(info)\n emit('progress', info.percent, info)\n}\n\nconst { fileKey, previewUrl, revokePreview, revokeAllPreviews } = useFilePreviews({\n enabled: () => props.preview,\n})\n\n/**\n * `extraData` считается один раз на набор и не меняется между файлами, поэтому\n * живёт рядом с очередью, а не путешествует через её контракт.\n */\nlet perFileExtraData: GrFileUploadExtraData | undefined\n\nconst perFile = usePerFileUpload({\n concurrency: () => props.concurrency,\n send: (files, ctx) => (props.request\n ? props.request(files, { signal: ctx.signal, extraData: perFileExtraData, onProgress: ctx.onProgress })\n : uploadViaXhr({\n url: props.action ?? '',\n files,\n name: props.name,\n headers: props.headers,\n withCredentials: props.withCredentials,\n extraData: perFileExtraData,\n signal: ctx.signal,\n onProgress: ctx.onProgress,\n })),\n onSummary: assignState,\n onProgress: (info, file) => emit('progress', info.percent, info, file),\n onSuccess: (payload, file) => emit('success', payload as TResponse, file),\n onError: (error, file) => emit('error', error, file),\n onSetChange: files => emit('change', files),\n})\n\nconst fileEntries = perFile.entries\nconst { retryFile, abortFile } = perFile\n\nconst dropZone = useDropZone({\n locked: () => isLocked.value,\n inactive: () => hasCustomUi.value,\n onDrop: files => void handleFiles(files, 'drop'),\n})\nconst isOver = dropZone.isOver\n\n/**\n * Дефолтный слот вызывается из двух мест — как своя зона целиком и как подпись\n * внутри стандартной. Набор пропов у него один: две копии разъехались бы.\n */\nconst defaultSlotProps = computed(() => ({\n openDialog,\n abort,\n disabled: isDisabled.value,\n files: lastFiles.value,\n isOver: isOver.value,\n state,\n retry,\n removeFile,\n fileEntries: fileEntries.value,\n retryFile,\n abortFile,\n}))\n\nconst zoneClass = computed(() => grFileUploadZoneClass({\n size: resolvedSize.value,\n disabled: isDisabled.value,\n readonly: isReadonly.value,\n over: isOver.value,\n}))\n\nconst hiddenInputStyle = {\n position: 'fixed',\n top: '-9999px',\n left: '-9999px',\n width: '1px',\n height: '1px',\n opacity: '0',\n overflow: 'hidden',\n pointerEvents: 'none',\n} as const satisfies Record<string, string>\n\nfunction openDialog() {\n if (isLocked.value)\n return\n inputRef.value?.click()\n}\n\nfunction abort() {\n activeController?.abort()\n activeController = null\n}\n\nfunction totalSizeOf(files: File[]): number {\n return files.reduce((sum, file) => sum + file.size, 0)\n}\n\nfunction normalizeLimit(limit: number | undefined): number | undefined {\n if (typeof limit !== 'number')\n return undefined\n if (!Number.isFinite(limit))\n return undefined\n if (limit <= 0)\n return undefined\n return Math.floor(limit)\n}\n\nasync function uploadViaAction(files: File[], signal: AbortSignal, extraData: GrFileUploadExtraData | undefined) {\n if (!props.action)\n throw new Error('GrFileUpload: either `action` or `request` must be provided')\n\n return uploadViaXhr({\n url: props.action,\n files,\n name: props.name,\n headers: props.headers,\n withCredentials: props.withCredentials,\n extraData,\n signal,\n onProgress: handleProgress,\n })\n}\n\nasync function runBeforeUpload(files: File[]): Promise<'ok' | { aborted: true, reason: unknown }> {\n if (!props.beforeUpload)\n return 'ok'\n\n for (const file of files) {\n try {\n const result = props.beforeUpload(file)\n\n if (result === false) {\n return { aborted: true, reason: new Error('Upload aborted by beforeUpload') }\n }\n\n if (result && typeof (result as any).then === 'function')\n await result\n }\n catch (error) {\n return { aborted: true, reason: error }\n }\n }\n\n return 'ok'\n}\n\n/**\n * `accept` идёт первым валидатором: атрибут на input фильтрует только диалог,\n * а drop мимо него проходит.\n */\nfunction effectiveValidators(): FileValidator[] {\n return [acceptValidator(props.accept), ...(props.validators ?? [])]\n}\n\n/**\n * Номер запуска. Валидация и `beforeUpload` асинхронны, поэтому два быстрых\n * выбора подряд идут внахлёст: без номера «побеждал» тот, чьи валидаторы\n * отработали позже, — он же обрывал уже стартовавшую загрузку соседа.\n * Актуален всегда последний выбор, остальные тихо сходят с дистанции.\n */\nlet runCounter = 0\n\nasync function handleFiles(files: File[], source: FileValidatorSource = 'input') {\n if (isLocked.value)\n return\n if (!files.length)\n return\n\n runCounter += 1\n const runId = runCounter\n const isStale = (): boolean => runId !== runCounter\n\n const normalizedLimit = normalizeLimit(props.limit)\n if (props.multiple && normalizedLimit && files.length > normalizedLimit) {\n emit('exceed', files, normalizedLimit)\n emit('error', new Error(`Too many files selected, limit=${normalizedLimit}`))\n return\n }\n\n const { files: valid, issues } = await runFileValidators(files, effectiveValidators(), {\n source,\n multiple: props.multiple,\n })\n\n if (isStale())\n return\n\n if (issues.length > 0) {\n emit('error', new FileValidationError(issues, valid))\n return\n }\n\n if (!valid.length)\n return\n\n // Набор сменился — миниатюры прежнего больше не нужны.\n revokeAllPreviews()\n setFiles(valid)\n\n const before = await runBeforeUpload(valid)\n if (isStale())\n return\n\n if (before !== 'ok') {\n emit('error', before.reason)\n return\n }\n\n let extraData: GrFileUploadExtraData | undefined\n try {\n extraData = props.uploadExtraData?.(valid)\n }\n catch (error) {\n emit('error', error)\n return\n }\n\n abort()\n clearScheduledIdle()\n\n if (props.uploadMode === 'per-file') {\n perFileExtraData = extraData\n await perFile.run(valid)\n return\n }\n\n const controller = new AbortController()\n activeController = controller\n\n // Батч: пофайловых записей нет, показываем набор без статусов.\n perFile.reset()\n\n setStateUploading()\n emit('progress', 0, { percent: 0, loaded: 0, total: 0, indeterminate: true })\n\n try {\n // `action`-ветка отдаёт разобранный ответ сервера: типом его знает только\n // потребитель, поэтому приводим к его же `TResponse`.\n const payload = (props.request\n ? await props.request(valid, { signal: controller.signal, extraData, onProgress: handleProgress })\n : await uploadViaAction(valid, controller.signal, extraData)) as TResponse\n\n // Кастомный `request` не обязан звать `onProgress` — тогда байты неизвестны.\n // Сумма размеров выбранных файлов честнее нуля: «100%» при `total: 0`\n // потребитель прочитает как «загружено ноль».\n const measured = state.phase === 'uploading' ? state.total || state.loaded : 0\n const finalLoaded = measured || totalSizeOf(valid)\n setStateSuccess({ loaded: finalLoaded, total: finalLoaded })\n emit('success', payload)\n emit('change', valid)\n emit('progress', 100, { percent: 100, loaded: finalLoaded, total: finalLoaded, indeterminate: false })\n\n if (props.hideProgressOnSuccess && props.hideProgressOnSuccess > 0)\n scheduleIdle(props.hideProgressOnSuccess)\n }\n catch (error) {\n if (error instanceof GrUploadAbortError) {\n setStateIdle()\n }\n else {\n setStateError(error)\n }\n emit('error', error)\n }\n finally {\n if (activeController === controller)\n activeController = null\n }\n}\n\n/**\n * `<input type=\"file\">` не понимает `readonly` — атрибута для него в HTML нет.\n * Поэтому системный диалог гасим отменой действия по умолчанию: input остаётся\n * в порядке `Tab` и объявляется как «только чтение», но нового файла не примет.\n */\nfunction onInputClick(event: MouseEvent) {\n if (isReadonly.value && !isDisabled.value)\n event.preventDefault()\n}\n\nasync function onInputChange(event: Event) {\n if (isLocked.value)\n return\n\n const target = event.target as HTMLInputElement | null\n const files = target?.files ? Array.prototype.slice.call(target.files) as File[] : []\n\n if (target)\n target.value = ''\n\n await nextTick()\n await handleFiles(files, 'input')\n}\n\n// Клавиатуры на зоне нет: Enter/Space обрабатывает сам file-input, на котором и\n// стоит фокус. Собственный обработчик здесь только дублировал бы нативный и\n// открывал диалог дважды.\nfunction onRootClick(event: MouseEvent) {\n if (hasCustomUi.value)\n return\n // `openDialog` кликает по input программно, и этот клик всплывает обратно сюда.\n // Без отсечки зона открывала бы диалог по кругу.\n if (event.target === inputRef.value)\n return\n\n openDialog()\n}\n\nconst effectiveProgressTone = computed<GrProgressBarTone>(() => {\n if (state.phase === 'error')\n return 'danger'\n if (state.phase === 'success')\n return 'success'\n return props.progressTone\n})\n\n/**\n * Живой регион существует с первого рендера и пуст, пока объявлять нечего:\n * регион, появляющийся сразу с текстом, часть AT не объявляет вовсе. Прогресс\n * в процентах сюда не идёт — диктор захлебнётся; объявляются только фазы.\n */\nconst liveMessage = computed(() => {\n if (state.phase === 'uploading')\n return t('gr.fileUpload.uploading', 'Uploading…')\n if (state.phase === 'success')\n return t('gr.fileUpload.success', 'Upload complete')\n if (state.phase === 'error')\n return t('gr.fileUpload.error', 'Upload failed')\n return ''\n})\n\nconst progressVisible = computed(() => state.phase !== 'idle')\nconst progressIndeterminate = computed(() => state.phase === 'uploading' && state.indeterminate)\nconst progressPercent = computed(() => (progressIndeterminate.value ? 0 : state.percent))\nconst progressText = computed(() => {\n if (progressIndeterminate.value)\n return ''\n return `${Math.round(progressPercent.value)}%`\n})\n\nconst isPerFile = computed(() => props.uploadMode === 'per-file')\n\nconst statusTextByStatus = computed<Record<string, string>>(() => ({\n pending: t('gr.fileUpload.statusPending', 'Pending'),\n uploading: t('gr.fileUpload.statusUploading', 'Uploading'),\n success: t('gr.fileUpload.statusSuccess', 'Uploaded'),\n error: t('gr.fileUpload.statusError', 'Failed'),\n}))\n\n/** Запись файла для разметки: в батчевом режиме её нет — и статуса тоже. */\nfunction entryFor(file: File): GrFileUploadEntry | undefined {\n return isPerFile.value ? perFile.entryOf(file) : undefined\n}\n\n/** Повторить загрузку текущего набора — после ошибки выбирать файлы заново незачем. */\nasync function retry(): Promise<void> {\n if (!lastFiles.value.length)\n return\n\n await handleFiles([...lastFiles.value], 'input')\n}\n\n/**\n * Убрать файл из набора. Идущая загрузка при этом обрывается: она была про\n * прежний набор, и её результат к новому отношения не имеет.\n */\nfunction removeFile(file: File): void {\n const next = lastFiles.value.filter(item => item !== file)\n if (next.length === lastFiles.value.length)\n return\n\n abortFile(file)\n abort()\n clearScheduledIdle()\n revokePreview(file)\n setFiles(next)\n perFile.dropEntry(file)\n\n // В пофайловом режиме остальные файлы своих статусов не теряют: удалили один\n // — сводное состояние просто пересчитывается по оставшимся.\n if (props.uploadMode === 'per-file' && fileEntries.value.length)\n assignState(summarizeFileEntries(fileEntries.value))\n else if (state.phase !== 'idle')\n setStateIdle()\n}\n\nfunction focus(): void {\n inputRef.value?.focus()\n}\n\nfunction blur(): void {\n inputRef.value?.blur()\n}\n\n// Ни `action`, ни `request` — загружать некуда, и узнать об этом на первом же\n// выборе файла поздно. Выразить требование типом нельзя: `defineProps` в SFC\n// принимает объектный тип или интерфейс, но не discriminated union.\nonMounted(() => {\n if (!__GR_DEV__)\n return\n if (props.action || props.request)\n return\n\n console.warn('[GrFileUpload] не задан ни `action`, ни `request`: отправлять файлы некуда.')\n})\n\n// Хвосты, переживающие размонтирование: незавершённый XHR продолжает качать\n// файл, а таймер скрытия успеха дёргает `setStateIdle()` на уничтоженном\n// инстансе. Сценарий обычный — успешная загрузка и уход со страницы сразу после.\nonBeforeUnmount(() => {\n clearScheduledIdle()\n abort()\n perFile.abortAll()\n revokeAllPreviews()\n})\n\ndefineExpose({\n focus,\n blur,\n uploadFiles: handleFiles,\n abort,\n openDialog,\n state,\n /** Текущий набор файлов — тот, что показан в списке и уйдёт в `retry`. */\n files: lastFiles,\n retry,\n removeFile,\n /** Записи по файлам — пусты в батчевом режиме. */\n fileEntries,\n retryFile,\n abortFile,\n})\n\n/**\n * Drop-zone намеренно БЕЗ `role=\"button\"` и `tabindex`: доступным контролом\n * служит сам нативный `<input type=\"file\">`. Роль-виджет объявляет потомков\n * презентационными, поэтому input внутри неё теряется для скринридеров и\n * падает в axe на `nested-interactive`; а обёртывать его в собственный\n * «button» — значит вручную переизобретать то, что file-input уже умеет\n * нативно (открытие диалога с клавиатуры, объявление «кнопка выбора файла»).\n * Зона остаётся кликабельной и принимает drag&drop, фокус input'а показывает\n * через `focus-within`.\n */\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-file-upload\n :class=\"hasCustomUi ? 'inline-block' : zoneClass\"\n @click=\"onRootClick\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n @dragenter=\"dropZone.onDragEnter\"\n @dragover=\"dropZone.onDragOver\"\n @dragleave=\"dropZone.onDragLeave\"\n @drop=\"dropZone.onDropFiles\"\n >\n <!-- В custom-UI контрол рисует слот (он и вызывает `openDialog`), поэтому там\n input прячется и от таба, и от дерева доступности — иначе получим два\n контрола на один смысл. -->\n <input\n :id=\"fieldId\"\n ref=\"inputRef\"\n data-gr-file-upload-input\n :style=\"hiddenInputStyle\"\n type=\"file\"\n :tabindex=\"hasCustomUi || isDisabled ? -1 : 0\"\n :aria-hidden=\"hasCustomUi ? 'true' : undefined\"\n :aria-label=\"ariaLabel ?? (hasCustomUi ? undefined : resolvedPlaceholder)\"\n :aria-describedby=\"describedBy\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :name=\"name\"\n :accept=\"accept\"\n :capture=\"capture\"\n :webkitdirectory=\"directory || undefined\"\n :multiple=\"multiple\"\n :disabled=\"isDisabled\"\n @click=\"onInputClick\"\n @change=\"onInputChange\"\n >\n\n <span\n data-gr-file-upload-live\n class=\"sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n >{{ liveMessage }}</span>\n\n <slot v-if=\"hasCustomUi\" v-bind=\"defaultSlotProps\" />\n\n <div v-else class=\"flex items-start\" :class=\"zoneGapClass\">\n <div\n data-gr-file-upload-icon\n class=\"shrink-0 bg-[var(--gr-muted)] border border-[var(--gr-brd)] flex items-center justify-center\"\n :class=\"iconTileClass\"\n aria-hidden=\"true\"\n >\n <GrIcon class=\"text-[var(--gr-muted-fg)]\" :size=\"iconGlyphSize\">\n <IconArrowUp />\n </GrIcon>\n </div>\n\n <div class=\"min-w-0\">\n <div data-gr-file-upload-label class=\"font-700\" :class=\"labelClass\">\n <slot name=\"label\">\n <slot v-bind=\"defaultSlotProps\">\n {{ resolvedPlaceholder }}\n </slot>\n </slot>\n </div>\n\n <div v-if=\"$slots.tip\" data-gr-file-upload-tip class=\"mt-1 text-[var(--gr-muted-fg)]\" :class=\"hintClass\">\n <slot name=\"tip\" />\n </div>\n <div v-else class=\"mt-1 text-[var(--gr-muted-fg)]\" :class=\"hintClass\" />\n\n <ul v-if=\"showFileList && lastFiles.length\" data-gr-file-upload-list class=\"mt-3 space-y-1\">\n <li\n v-for=\"file in lastFiles\"\n :key=\"fileKey(file)\"\n data-gr-file-upload-item\n class=\"flex items-center gap-2\"\n :class=\"hintClass\"\n >\n <img\n v-if=\"previewUrl(file)\"\n data-gr-file-upload-preview\n :src=\"previewUrl(file)\"\n alt=\"\"\n class=\"h-8 w-8 shrink-0 rounded-[var(--gr-radius-sm)] object-cover border border-[var(--gr-brd)]\"\n >\n <span class=\"font-600\">{{ file.name }}</span>\n <span class=\"text-[var(--gr-muted-fg)]\"> · {{ Math.ceil(file.size / 1024) }} KB</span>\n\n <template v-if=\"entryFor(file)\">\n <span\n data-gr-file-upload-status\n :data-status=\"entryFor(file)!.status\"\n class=\"text-[var(--gr-muted-fg)]\"\n >· {{ statusTextByStatus[entryFor(file)!.status] }}<template\n v-if=\"entryFor(file)!.status === 'uploading'\"\n >&nbsp;{{ Math.round(entryFor(file)!.percent) }}%</template></span>\n\n <button\n v-if=\"entryFor(file)!.status === 'uploading'\"\n data-gr-file-upload-abort-file\n type=\"button\"\n class=\"ml-auto text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] rounded-[var(--gr-radius-sm)]\"\n :aria-label=\"t('gr.fileUpload.abortFile', 'Cancel upload of {fileName}', { fileName: file.name })\"\n @click.stop=\"abortFile(file)\"\n >\n <GrIcon :size=\"iconGlyphSize\">\n <IconClose />\n </GrIcon>\n </button>\n\n <button\n v-else-if=\"entryFor(file)!.status === 'error' && !isDisabled && !isReadonly\"\n data-gr-file-upload-retry-file\n type=\"button\"\n class=\"ml-auto text-[var(--gr-danger-text)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] rounded-[var(--gr-radius-sm)] underline\"\n :aria-label=\"t('gr.fileUpload.retryFile', 'Retry {fileName}', { fileName: file.name })\"\n @click.stop=\"retryFile(file)\"\n >\n {{ t('gr.fileUpload.retry', 'Retry upload') }}\n </button>\n </template>\n\n <button\n v-if=\"!isDisabled && !isReadonly\"\n data-gr-file-upload-remove\n type=\"button\"\n class=\"ml-auto text-[var(--gr-muted-fg)] hover:text-[var(--gr-danger-text)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] rounded-[var(--gr-radius-sm)]\"\n :aria-label=\"t('gr.fileUpload.remove', 'Remove {fileName}', { fileName: file.name })\"\n @click.stop=\"removeFile(file)\"\n >\n <GrIcon :size=\"iconGlyphSize\">\n <IconClose />\n </GrIcon>\n </button>\n </li>\n </ul>\n\n <slot\n v-if=\"showProgress || $slots.progress\"\n name=\"progress\"\n :state=\"state\"\n :percent=\"state.percent\"\n :indeterminate=\"state.phase === 'uploading' && state.indeterminate\"\n :phase=\"state.phase\"\n :files=\"lastFiles\"\n :abort=\"abort\"\n :retry=\"retry\"\n :file-entries=\"fileEntries\"\n :retry-file=\"retryFile\"\n :abort-file=\"abortFile\"\n >\n <div\n v-if=\"showProgress\"\n data-gr-file-upload-progress\n class=\"mt-3 transition-opacity duration-[var(--gr-duration-fast)]\"\n :class=\"progressVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'\"\n :aria-hidden=\"!progressVisible\"\n >\n <div class=\"flex items-center gap-2\">\n <GrProgressBar\n :value=\"progressPercent\"\n :indeterminate=\"progressIndeterminate\"\n :tone=\"effectiveProgressTone\"\n :size=\"progressBarSize\"\n :aria-label=\"resolvedProgressLabel\"\n />\n <span\n data-gr-file-upload-progress-text\n class=\"text-[var(--gr-muted-fg)] tabular-nums min-w-[3ch] text-right\"\n :class=\"progressTextClass\"\n >{{ progressText }}</span>\n </div>\n </div>\n </slot>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\" generic=\"TResponse = unknown\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, Text, useSlots, watch, type VNode } from 'vue'\n\nimport IconArrowUp from '~icons/lucide/arrow-up'\nimport IconClose from '~icons/lucide/x'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport GrProgressBar from '../GrProgressBar/GrProgressBar.vue'\nimport type { GrProgressBarTone } from '../GrProgressBar/grStyle'\nimport type { FileValidator, FileValidatorSource } from '../../fileValidation'\nimport type { GrUploadProgressInfo } from './uploadViaXhr'\nimport type { GrUploadState } from './uploadState'\nimport { summarizeFileEntries, type GrFileUploadEntry } from './fileEntry'\nimport { useDropZone } from './useDropZone'\nimport { useFilePreviews } from '../../composables/internal/useFilePreviews'\nimport { usePerFileUpload } from './usePerFileUpload'\nimport { useUploadState } from './useUploadState'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { flattenSlotNodes, meaningfulSlotNodes } from '../shared/slotNodes'\nimport {\n type GrFileUploadSize,\n grFileUploadZoneClass,\n hintSizes,\n iconGlyphSizes,\n iconTileSizes,\n labelSizes,\n progressBarSizes,\n progressTextSizes,\n zoneGaps,\n} from './grFileUploadStyles'\nimport { acceptValidator, FileValidationError, runFileValidators } from '../../fileValidation'\nimport { GrUploadAbortError, uploadViaXhr } from './uploadViaXhr'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nexport type GrFileUploadMode = 'batch' | 'per-file'\n\nexport type GrFileUploadExtraDataValue = string | Blob\n\nexport type GrFileUploadExtraData = Record<string, GrFileUploadExtraDataValue | GrFileUploadExtraDataValue[]>\n\nexport type GrFileUploadRequestCtx = {\n signal: AbortSignal\n extraData?: GrFileUploadExtraData\n /**\n * Колбэк прогресса аплоада. Пользовательский `request` (например, axios)\n * должен вызывать его из `onUploadProgress`, чтобы `GrFileUpload` отображал\n * прогресс через слот `progress` / дефолтный `GrProgressBar`.\n */\n onProgress?: (info: GrUploadProgressInfo) => void\n}\n\nexport type GrFileUploadRequest<TResponse = unknown> = (\n files: File[],\n ctx: GrFileUploadRequestCtx,\n) => Promise<TResponse>\n\n/**\n * Пропы `GrFileUpload`.\n *\n * Либо `action` (URL для POST multipart/form-data), либо `request` — кастомный\n * загрузчик (например, через axios). Если переданы оба — приоритет у `request`.\n *\n * `placeholder` — надпись в дефолтном UI-варианте (без слота default).\n */\nexport interface GrFileUploadProps<TResponse = unknown> {\n /**\n * Набор файлов — тот, что показан в списке и уйдёт в `retry`.\n *\n * Проп **необязателен**: без него компонент держит набор сам. Передан —\n * набор следует за ним, и потребитель может очистить или подменить список\n * снаружи.\n */\n modelValue?: File[]\n action?: string\n request?: GrFileUploadRequest<TResponse>\n name?: string\n multiple?: boolean\n limit?: number\n /**\n * Guard перед отправкой. Остаётся пропом, а не эмитом, осознанно: эмит не\n * возвращает значения, а этот колбэк обязан ответить «пускать или нет».\n * Уведомления — `exceed`, `success`, `error`, `progress` — эмиты.\n */\n beforeUpload?: (file: File) => boolean | Promise<unknown>\n validators?: FileValidator[]\n /**\n * W3C `accept` для `<input type=\"file\">` — и sugar к `acceptValidator(...)`.\n *\n * Только атрибута мало: он фильтрует системный диалог, но не drag&drop —\n * перетащить можно что угодно. Поэтому то же значение уходит и в валидаторы,\n * как в `GrFormFile`.\n */\n accept?: string\n /** `capture` для мобильной камеры/микрофона. */\n capture?: 'user' | 'environment'\n /** Выбор каталога целиком (`webkitdirectory`). Поддержка — Chromium и Safari. */\n directory?: boolean\n disabled?: boolean\n /** Только для чтения: значение видно и уходит в форму, но не редактируется. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n headers?: Record<string, string>\n withCredentials?: boolean\n showFileList?: boolean\n uploadExtraData?: (files: File[]) => GrFileUploadExtraData | undefined\n /** i18n: надпись-подсказка в дефолтном UI. */\n placeholder?: string\n /** Показывать дефолтный прогресс-бар (если не используется слот `progress`). */\n showProgress?: boolean\n /** Цветовой тон полосы прогресса в фазе `uploading`. */\n progressTone?: GrProgressBarTone\n /** aria-label для прогресс-бара. */\n progressLabel?: string\n /** Через сколько мс после `success` скрыть прогресс-бар. `0` — не скрывать. */\n hideProgressOnSuccess?: number\n /** Размер дефолтного UI: поля дроп-зоны, плитка иконки, кегль подписей. */\n size?: GrFileUploadSize\n /**\n * Как уходит набор файлов:\n *\n * - `batch` (по умолчанию) — весь набор одним запросом. Про отдельный файл\n * сказать нечего, поэтому и статуса у него нет;\n * - `per-file` — на каждый файл свой запрос (`request` зовётся с массивом из\n * одного файла, контракт не меняется). Появляются статус и процент строки,\n * `retryFile` и `abortFile`.\n */\n uploadMode?: GrFileUploadMode\n /** Сколько файлов грузится одновременно в режиме `per-file`. */\n concurrency?: number\n /** Миниатюры для `image/*` в списке файлов. */\n preview?: boolean\n}\n\nexport interface GrFileUploadEmits<TResponse = unknown> {\n /** Набор файлов сменился: новый выбор или удаление. Не путать с `change`. */\n (e: 'update:modelValue', files: File[]): void\n /** Выбрано больше файлов, чем разрешает `limit`. Загрузка не стартует. */\n (e: 'exceed', files: File[], limit: number): void\n /** `file` приходит только в режиме `per-file`: в батче отчитываться нечем. */\n (e: 'success', payload: TResponse, file?: File): void\n (e: 'error', error: unknown, file?: File): void\n (e: 'progress', percent: number, info?: GrUploadProgressInfo, file?: File): void\n (e: 'change', files: File[]): void\n (e: 'stateChange', state: GrUploadState): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrFileUploadProps<TResponse>>(),\n {\n modelValue: undefined,\n action: undefined,\n request: undefined,\n name: 'file',\n multiple: false,\n limit: undefined,\n beforeUpload: undefined,\n validators: undefined,\n accept: undefined,\n capture: undefined,\n directory: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n headers: undefined,\n withCredentials: false,\n showFileList: false,\n uploadExtraData: undefined,\n placeholder: undefined,\n showProgress: true,\n progressTone: 'primary',\n progressLabel: undefined,\n hideProgressOnSuccess: 800,\n size: undefined,\n uploadMode: 'batch',\n concurrency: 3,\n preview: false,\n },\n)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFileUpload' })\n\nconst zoneGapClass = computed(() => zoneGaps[resolvedSize.value])\nconst iconTileClass = computed(() => iconTileSizes[resolvedSize.value])\nconst iconGlyphSize = computed(() => iconGlyphSizes[resolvedSize.value])\nconst labelClass = computed(() => labelSizes[resolvedSize.value])\nconst hintClass = computed(() => hintSizes[resolvedSize.value])\nconst progressTextClass = computed(() => progressTextSizes[resolvedSize.value])\nconst progressBarSize = computed(() => progressBarSizes[resolvedSize.value])\n\nconst emit = defineEmits<GrFileUploadEmits<TResponse>>()\ndefineSlots<{\n /** Полностью своя зона загрузки: контрол отдаёт наружу всё своё состояние. */\n default?: (props: {\n openDialog: () => void\n abort: () => void\n disabled: boolean\n files: File[]\n isOver: boolean\n state: GrUploadState\n retry: () => Promise<void>\n removeFile: (file: File) => void\n fileEntries: GrFileUploadEntry[]\n retryFile: (file: File) => Promise<void>\n abortFile: (file: File) => void\n }) => any\n /** Заголовок зоны вместо `placeholder`. */\n label?: () => any\n /** Подпись под заголовком: ограничения по типу и размеру. */\n tip?: () => any\n /** Свой индикатор прогресса вместо встроенного. */\n progress?: (props: {\n state: GrUploadState\n percent: number\n indeterminate: boolean\n phase: GrUploadState['phase']\n files: File[]\n abort: () => void\n retry: () => Promise<void>\n fileEntries: GrFileUploadEntry[]\n retryFile: (file: File) => Promise<void>\n abortFile: (file: File) => void\n }) => any\n}>()\n\nconst slots = useSlots()\n\nconst { t } = useGranularityTranslations()\n\n// Контекст `GrFormField`: доступным контролом служит сам нативный\n// `<input type=\"file\">` — он и остаётся целью `<label for>`.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n readonly: isReadonly,\n invalid: isInvalid,\n required: isRequired,\n // `locked` — «ввод не принимается»: `disabled` или `readonly`. Набор в\n // `readonly` виден и уходит в форму, но поменять его нельзя.\n locked: isLocked,\n} = useGrFormControl(() => props)\nconst resolvedPlaceholder = computed(() => props.placeholder ?? t('gr.fileUpload.placeholder', 'Drag files here or click to select'))\nconst resolvedProgressLabel = computed(() => props.progressLabel ?? t('gr.fileUpload.progress', 'Upload progress'))\n\nfunction slotIsTextOnly(nodes: VNode[]): boolean {\n const meaningful = meaningfulSlotNodes(nodes)\n\n if (meaningful.length === 0)\n return true\n\n for (const node of meaningful) {\n if (node.type !== Text)\n return false\n }\n\n return true\n}\n\nconst defaultSlotNodes = computed(() => {\n return slots.default ? flattenSlotNodes(slots.default({} as any)) : []\n})\n\nconst hasCustomUi = computed(() => {\n return !!slots.default && !slotIsTextOnly(defaultSlotNodes.value)\n})\n\nconst inputRef = ref<HTMLInputElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Внутри зоны фокус ходит между нативным input'ом и кнопками списка файлов:\n// без границы каждое перемещение давало бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n\nconst lastFiles = ref<File[]>(props.modelValue ? [...props.modelValue] : [])\n\n/**\n * Набор меняется только здесь: иначе `update:modelValue` рано или поздно\n * разошёлся бы с тем, что показано в списке.\n */\nfunction setFiles(next: File[]): void {\n lastFiles.value = next\n emit('update:modelValue', next)\n}\n\n// Контролируемый режим: проп задан — он и есть источник истины.\nwatch(() => props.modelValue, (next) => {\n if (next === undefined)\n return\n if (next === lastFiles.value)\n return\n\n lastFiles.value = [...next]\n}, { deep: false })\n\nconst {\n state,\n setIdle: setStateIdle,\n setUploading: setStateUploading,\n setSuccess: setStateSuccess,\n setError: setStateError,\n applyProgress,\n assign: assignState,\n scheduleIdle,\n clearScheduledIdle,\n} = useUploadState({ onChange: next => emit('stateChange', next) })\n\nlet activeController: AbortController | null = null\n\nfunction handleProgress(info: GrUploadProgressInfo) {\n applyProgress(info)\n emit('progress', info.percent, info)\n}\n\nconst { fileKey, previewUrl, revokePreview, revokeAllPreviews } = useFilePreviews({\n enabled: () => props.preview,\n})\n\n/**\n * `extraData` считается один раз на набор и не меняется между файлами, поэтому\n * живёт рядом с очередью, а не путешествует через её контракт.\n */\nlet perFileExtraData: GrFileUploadExtraData | undefined\n\nconst perFile = usePerFileUpload({\n concurrency: () => props.concurrency,\n send: (files, ctx) => (props.request\n ? props.request(files, { signal: ctx.signal, extraData: perFileExtraData, onProgress: ctx.onProgress })\n : uploadViaXhr({\n url: props.action ?? '',\n files,\n name: props.name,\n headers: props.headers,\n withCredentials: props.withCredentials,\n extraData: perFileExtraData,\n signal: ctx.signal,\n onProgress: ctx.onProgress,\n })),\n onSummary: assignState,\n onProgress: (info, file) => emit('progress', info.percent, info, file),\n onSuccess: (payload, file) => emit('success', payload as TResponse, file),\n onError: (error, file) => emit('error', error, file),\n onSetChange: files => emit('change', files),\n})\n\nconst fileEntries = perFile.entries\nconst { retryFile, abortFile } = perFile\n\nconst dropZone = useDropZone({\n locked: () => isLocked.value,\n inactive: () => hasCustomUi.value,\n onDrop: files => void handleFiles(files, 'drop'),\n})\nconst isOver = dropZone.isOver\n\n/**\n * Дефолтный слот вызывается из двух мест — как своя зона целиком и как подпись\n * внутри стандартной. Набор пропов у него один: две копии разъехались бы.\n */\nconst defaultSlotProps = computed(() => ({\n openDialog,\n abort,\n disabled: isDisabled.value,\n files: lastFiles.value,\n isOver: isOver.value,\n state,\n retry,\n removeFile,\n fileEntries: fileEntries.value,\n retryFile,\n abortFile,\n}))\n\nconst zoneClass = computed(() => grFileUploadZoneClass({\n size: resolvedSize.value,\n disabled: isDisabled.value,\n readonly: isReadonly.value,\n over: isOver.value,\n}))\n\nconst hiddenInputStyle = {\n position: 'fixed',\n top: '-9999px',\n left: '-9999px',\n width: '1px',\n height: '1px',\n opacity: '0',\n overflow: 'hidden',\n pointerEvents: 'none',\n} as const satisfies Record<string, string>\n\nfunction openDialog() {\n if (isLocked.value)\n return\n inputRef.value?.click()\n}\n\nfunction abort() {\n activeController?.abort()\n activeController = null\n}\n\nfunction totalSizeOf(files: File[]): number {\n return files.reduce((sum, file) => sum + file.size, 0)\n}\n\nfunction normalizeLimit(limit: number | undefined): number | undefined {\n if (typeof limit !== 'number')\n return undefined\n if (!Number.isFinite(limit))\n return undefined\n if (limit <= 0)\n return undefined\n return Math.floor(limit)\n}\n\nasync function uploadViaAction(files: File[], signal: AbortSignal, extraData: GrFileUploadExtraData | undefined) {\n if (!props.action)\n throw new Error('GrFileUpload: either `action` or `request` must be provided')\n\n return uploadViaXhr({\n url: props.action,\n files,\n name: props.name,\n headers: props.headers,\n withCredentials: props.withCredentials,\n extraData,\n signal,\n onProgress: handleProgress,\n })\n}\n\nasync function runBeforeUpload(files: File[]): Promise<'ok' | { aborted: true, reason: unknown }> {\n if (!props.beforeUpload)\n return 'ok'\n\n for (const file of files) {\n try {\n const result = props.beforeUpload(file)\n\n if (result === false) {\n return { aborted: true, reason: new Error('Upload aborted by beforeUpload') }\n }\n\n if (result && typeof (result as any).then === 'function')\n await result\n }\n catch (error) {\n return { aborted: true, reason: error }\n }\n }\n\n return 'ok'\n}\n\n/**\n * `accept` идёт первым валидатором: атрибут на input фильтрует только диалог,\n * а drop мимо него проходит.\n */\nfunction effectiveValidators(): FileValidator[] {\n return [acceptValidator(props.accept), ...(props.validators ?? [])]\n}\n\n/**\n * Номер запуска. Валидация и `beforeUpload` асинхронны, поэтому два быстрых\n * выбора подряд идут внахлёст: без номера «побеждал» тот, чьи валидаторы\n * отработали позже, — он же обрывал уже стартовавшую загрузку соседа.\n * Актуален всегда последний выбор, остальные тихо сходят с дистанции.\n */\nlet runCounter = 0\n\nasync function handleFiles(files: File[], source: FileValidatorSource = 'input') {\n if (isLocked.value)\n return\n if (!files.length)\n return\n\n runCounter += 1\n const runId = runCounter\n const isStale = (): boolean => runId !== runCounter\n\n const normalizedLimit = normalizeLimit(props.limit)\n if (props.multiple && normalizedLimit && files.length > normalizedLimit) {\n emit('exceed', files, normalizedLimit)\n emit('error', new Error(`Too many files selected, limit=${normalizedLimit}`))\n return\n }\n\n const { files: valid, issues } = await runFileValidators(files, effectiveValidators(), {\n source,\n multiple: props.multiple,\n })\n\n if (isStale())\n return\n\n if (issues.length > 0) {\n emit('error', new FileValidationError(issues, valid))\n return\n }\n\n if (!valid.length)\n return\n\n // Набор сменился — миниатюры прежнего больше не нужны.\n revokeAllPreviews()\n setFiles(valid)\n\n const before = await runBeforeUpload(valid)\n if (isStale())\n return\n\n if (before !== 'ok') {\n emit('error', before.reason)\n return\n }\n\n let extraData: GrFileUploadExtraData | undefined\n try {\n extraData = props.uploadExtraData?.(valid)\n }\n catch (error) {\n emit('error', error)\n return\n }\n\n abort()\n clearScheduledIdle()\n\n if (props.uploadMode === 'per-file') {\n perFileExtraData = extraData\n await perFile.run(valid)\n return\n }\n\n const controller = new AbortController()\n activeController = controller\n\n // Батч: пофайловых записей нет, показываем набор без статусов.\n perFile.reset()\n\n setStateUploading()\n emit('progress', 0, { percent: 0, loaded: 0, total: 0, indeterminate: true })\n\n try {\n // `action`-ветка отдаёт разобранный ответ сервера: типом его знает только\n // потребитель, поэтому приводим к его же `TResponse`.\n const payload = (props.request\n ? await props.request(valid, { signal: controller.signal, extraData, onProgress: handleProgress })\n : await uploadViaAction(valid, controller.signal, extraData)) as TResponse\n\n // Кастомный `request` не обязан звать `onProgress` — тогда байты неизвестны.\n // Сумма размеров выбранных файлов честнее нуля: «100%» при `total: 0`\n // потребитель прочитает как «загружено ноль».\n const measured = state.phase === 'uploading' ? state.total || state.loaded : 0\n const finalLoaded = measured || totalSizeOf(valid)\n setStateSuccess({ loaded: finalLoaded, total: finalLoaded })\n emit('success', payload)\n emit('change', valid)\n emit('progress', 100, { percent: 100, loaded: finalLoaded, total: finalLoaded, indeterminate: false })\n\n if (props.hideProgressOnSuccess && props.hideProgressOnSuccess > 0)\n scheduleIdle(props.hideProgressOnSuccess)\n }\n catch (error) {\n if (error instanceof GrUploadAbortError) {\n setStateIdle()\n }\n else {\n setStateError(error)\n }\n emit('error', error)\n }\n finally {\n if (activeController === controller)\n activeController = null\n }\n}\n\n/**\n * `<input type=\"file\">` не понимает `readonly` — атрибута для него в HTML нет.\n * Поэтому системный диалог гасим отменой действия по умолчанию: input остаётся\n * в порядке `Tab` и объявляется как «только чтение», но нового файла не примет.\n */\nfunction onInputClick(event: MouseEvent) {\n if (isReadonly.value && !isDisabled.value)\n event.preventDefault()\n}\n\nasync function onInputChange(event: Event) {\n if (isLocked.value)\n return\n\n const target = event.target as HTMLInputElement | null\n const files = target?.files ? Array.prototype.slice.call(target.files) as File[] : []\n\n if (target)\n target.value = ''\n\n await nextTick()\n await handleFiles(files, 'input')\n}\n\n// Клавиатуры на зоне нет: Enter/Space обрабатывает сам file-input, на котором и\n// стоит фокус. Собственный обработчик здесь только дублировал бы нативный и\n// открывал диалог дважды.\nfunction onRootClick(event: MouseEvent) {\n if (hasCustomUi.value)\n return\n // `openDialog` кликает по input программно, и этот клик всплывает обратно сюда.\n // Без отсечки зона открывала бы диалог по кругу.\n if (event.target === inputRef.value)\n return\n\n openDialog()\n}\n\nconst effectiveProgressTone = computed<GrProgressBarTone>(() => {\n if (state.phase === 'error')\n return 'danger'\n if (state.phase === 'success')\n return 'success'\n return props.progressTone\n})\n\n/**\n * Живой регион существует с первого рендера и пуст, пока объявлять нечего:\n * регион, появляющийся сразу с текстом, часть AT не объявляет вовсе. Прогресс\n * в процентах сюда не идёт — диктор захлебнётся; объявляются только фазы.\n */\nconst liveMessage = computed(() => {\n if (state.phase === 'uploading')\n return t('gr.fileUpload.uploading', 'Uploading…')\n if (state.phase === 'success')\n return t('gr.fileUpload.success', 'Upload complete')\n if (state.phase === 'error')\n return t('gr.fileUpload.error', 'Upload failed')\n return ''\n})\n\nconst progressVisible = computed(() => state.phase !== 'idle')\nconst progressIndeterminate = computed(() => state.phase === 'uploading' && state.indeterminate)\nconst progressPercent = computed(() => (progressIndeterminate.value ? 0 : state.percent))\nconst progressText = computed(() => {\n if (progressIndeterminate.value)\n return ''\n return `${Math.round(progressPercent.value)}%`\n})\n\nconst isPerFile = computed(() => props.uploadMode === 'per-file')\n\nconst statusTextByStatus = computed<Record<string, string>>(() => ({\n pending: t('gr.fileUpload.statusPending', 'Pending'),\n uploading: t('gr.fileUpload.statusUploading', 'Uploading'),\n success: t('gr.fileUpload.statusSuccess', 'Uploaded'),\n error: t('gr.fileUpload.statusError', 'Failed'),\n}))\n\n/** Запись файла для разметки: в батчевом режиме её нет — и статуса тоже. */\nfunction entryFor(file: File): GrFileUploadEntry | undefined {\n return isPerFile.value ? perFile.entryOf(file) : undefined\n}\n\n/** Повторить загрузку текущего набора — после ошибки выбирать файлы заново незачем. */\nasync function retry(): Promise<void> {\n if (!lastFiles.value.length)\n return\n\n await handleFiles([...lastFiles.value], 'input')\n}\n\n/**\n * Убрать файл из набора. Идущая загрузка при этом обрывается: она была про\n * прежний набор, и её результат к новому отношения не имеет.\n */\nfunction removeFile(file: File): void {\n const next = lastFiles.value.filter(item => item !== file)\n if (next.length === lastFiles.value.length)\n return\n\n abortFile(file)\n abort()\n clearScheduledIdle()\n revokePreview(file)\n setFiles(next)\n perFile.dropEntry(file)\n\n // В пофайловом режиме остальные файлы своих статусов не теряют: удалили один\n // — сводное состояние просто пересчитывается по оставшимся.\n if (props.uploadMode === 'per-file' && fileEntries.value.length)\n assignState(summarizeFileEntries(fileEntries.value))\n else if (state.phase !== 'idle')\n setStateIdle()\n}\n\nfunction focus(): void {\n inputRef.value?.focus()\n}\n\nfunction blur(): void {\n inputRef.value?.blur()\n}\n\n// Ни `action`, ни `request` — загружать некуда, и узнать об этом на первом же\n// выборе файла поздно. Выразить требование типом нельзя: `defineProps` в SFC\n// принимает объектный тип или интерфейс, но не discriminated union.\nonMounted(() => {\n if (!__GR_DEV__)\n return\n if (props.action || props.request)\n return\n\n console.warn('[GrFileUpload] не задан ни `action`, ни `request`: отправлять файлы некуда.')\n})\n\n// Хвосты, переживающие размонтирование: незавершённый XHR продолжает качать\n// файл, а таймер скрытия успеха дёргает `setStateIdle()` на уничтоженном\n// инстансе. Сценарий обычный — успешная загрузка и уход со страницы сразу после.\nonBeforeUnmount(() => {\n clearScheduledIdle()\n abort()\n perFile.abortAll()\n revokeAllPreviews()\n})\n\ndefineExpose({\n focus,\n blur,\n uploadFiles: handleFiles,\n abort,\n openDialog,\n state,\n /** Текущий набор файлов — тот, что показан в списке и уйдёт в `retry`. */\n files: lastFiles,\n retry,\n removeFile,\n /** Записи по файлам — пусты в батчевом режиме. */\n fileEntries,\n retryFile,\n abortFile,\n})\n\n/**\n * Drop-zone намеренно БЕЗ `role=\"button\"` и `tabindex`: доступным контролом\n * служит сам нативный `<input type=\"file\">`. Роль-виджет объявляет потомков\n * презентационными, поэтому input внутри неё теряется для скринридеров и\n * падает в axe на `nested-interactive`; а обёртывать его в собственный\n * «button» — значит вручную переизобретать то, что file-input уже умеет\n * нативно (открытие диалога с клавиатуры, объявление «кнопка выбора файла»).\n * Зона остаётся кликабельной и принимает drag&drop, фокус input'а показывает\n * через `focus-within`.\n */\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-file-upload\n :class=\"hasCustomUi ? 'inline-block' : zoneClass\"\n @click=\"onRootClick\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n @dragenter=\"dropZone.onDragEnter\"\n @dragover=\"dropZone.onDragOver\"\n @dragleave=\"dropZone.onDragLeave\"\n @drop=\"dropZone.onDropFiles\"\n >\n <!-- В custom-UI контрол рисует слот (он и вызывает `openDialog`), поэтому там\n input прячется и от таба, и от дерева доступности — иначе получим два\n контрола на один смысл. -->\n <input\n :id=\"fieldId\"\n ref=\"inputRef\"\n data-gr-file-upload-input\n :style=\"hiddenInputStyle\"\n type=\"file\"\n :tabindex=\"hasCustomUi || isDisabled ? -1 : 0\"\n :aria-hidden=\"hasCustomUi ? 'true' : undefined\"\n :aria-label=\"ariaLabel ?? (hasCustomUi ? undefined : resolvedPlaceholder)\"\n :aria-describedby=\"describedBy\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :name=\"name\"\n :accept=\"accept\"\n :capture=\"capture\"\n :webkitdirectory=\"directory || undefined\"\n :multiple=\"multiple\"\n :disabled=\"isDisabled\"\n @click=\"onInputClick\"\n @change=\"onInputChange\"\n >\n\n <span\n data-gr-file-upload-live\n class=\"sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n >{{ liveMessage }}</span>\n\n <slot v-if=\"hasCustomUi\" v-bind=\"defaultSlotProps\" />\n\n <div v-else class=\"flex items-start\" :class=\"zoneGapClass\">\n <div\n data-gr-file-upload-icon\n class=\"shrink-0 bg-[var(--gr-muted)] border border-[var(--gr-brd)] flex items-center justify-center\"\n :class=\"iconTileClass\"\n aria-hidden=\"true\"\n >\n <GrIcon class=\"text-[var(--gr-muted-fg)]\" :size=\"iconGlyphSize\">\n <IconArrowUp />\n </GrIcon>\n </div>\n\n <div class=\"min-w-0\">\n <div data-gr-file-upload-label class=\"font-700\" :class=\"labelClass\">\n <slot name=\"label\">\n <slot v-bind=\"defaultSlotProps\">\n {{ resolvedPlaceholder }}\n </slot>\n </slot>\n </div>\n\n <div v-if=\"$slots.tip\" data-gr-file-upload-tip class=\"mt-1 text-[var(--gr-muted-fg)]\" :class=\"hintClass\">\n <slot name=\"tip\" />\n </div>\n <div v-else class=\"mt-1 text-[var(--gr-muted-fg)]\" :class=\"hintClass\" />\n\n <ul v-if=\"showFileList && lastFiles.length\" data-gr-file-upload-list class=\"mt-3 space-y-1\">\n <li\n v-for=\"file in lastFiles\"\n :key=\"fileKey(file)\"\n data-gr-file-upload-item\n class=\"flex items-center gap-2\"\n :class=\"hintClass\"\n >\n <img\n v-if=\"previewUrl(file)\"\n data-gr-file-upload-preview\n :src=\"previewUrl(file)\"\n alt=\"\"\n class=\"h-8 w-8 shrink-0 rounded-[var(--gr-radius-sm)] object-cover border border-[var(--gr-brd)]\"\n >\n <span class=\"font-600\">{{ file.name }}</span>\n <span class=\"text-[var(--gr-muted-fg)]\"> · {{ Math.ceil(file.size / 1024) }} KB</span>\n\n <template v-if=\"entryFor(file)\">\n <span\n data-gr-file-upload-status\n :data-status=\"entryFor(file)!.status\"\n class=\"text-[var(--gr-muted-fg)]\"\n >· {{ statusTextByStatus[entryFor(file)!.status] }}<template\n v-if=\"entryFor(file)!.status === 'uploading'\"\n >&nbsp;{{ Math.round(entryFor(file)!.percent) }}%</template></span>\n\n <button\n v-if=\"entryFor(file)!.status === 'uploading'\"\n data-gr-file-upload-abort-file\n type=\"button\"\n class=\"ml-auto text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] rounded-[var(--gr-radius-sm)]\"\n :aria-label=\"t('gr.fileUpload.abortFile', 'Cancel upload of {fileName}', { fileName: file.name })\"\n @click.stop=\"abortFile(file)\"\n >\n <GrIcon :size=\"iconGlyphSize\">\n <IconClose />\n </GrIcon>\n </button>\n\n <button\n v-else-if=\"entryFor(file)!.status === 'error' && !isDisabled && !isReadonly\"\n data-gr-file-upload-retry-file\n type=\"button\"\n class=\"ml-auto text-[var(--gr-danger-text)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] rounded-[var(--gr-radius-sm)] underline\"\n :aria-label=\"t('gr.fileUpload.retryFile', 'Retry {fileName}', { fileName: file.name })\"\n @click.stop=\"retryFile(file)\"\n >\n {{ t('gr.fileUpload.retry', 'Retry upload') }}\n </button>\n </template>\n\n <button\n v-if=\"!isDisabled && !isReadonly\"\n data-gr-file-upload-remove\n type=\"button\"\n class=\"ml-auto text-[var(--gr-muted-fg)] hover:text-[var(--gr-danger-text)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] rounded-[var(--gr-radius-sm)]\"\n :aria-label=\"t('gr.fileUpload.remove', 'Remove {fileName}', { fileName: file.name })\"\n @click.stop=\"removeFile(file)\"\n >\n <GrIcon :size=\"iconGlyphSize\">\n <IconClose />\n </GrIcon>\n </button>\n </li>\n </ul>\n\n <slot\n v-if=\"showProgress || $slots.progress\"\n name=\"progress\"\n :state=\"state\"\n :percent=\"state.percent\"\n :indeterminate=\"state.phase === 'uploading' && state.indeterminate\"\n :phase=\"state.phase\"\n :files=\"lastFiles\"\n :abort=\"abort\"\n :retry=\"retry\"\n :file-entries=\"fileEntries\"\n :retry-file=\"retryFile\"\n :abort-file=\"abortFile\"\n >\n <div\n v-if=\"showProgress\"\n data-gr-file-upload-progress\n class=\"mt-3 transition-opacity duration-[var(--gr-duration-fast)]\"\n :class=\"progressVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'\"\n :aria-hidden=\"!progressVisible\"\n >\n <div class=\"flex items-center gap-2\">\n <GrProgressBar\n :value=\"progressPercent\"\n :indeterminate=\"progressIndeterminate\"\n :tone=\"effectiveProgressTone\"\n :size=\"progressBarSize\"\n :aria-label=\"resolvedProgressLabel\"\n />\n <span\n data-gr-file-upload-progress-text\n class=\"text-[var(--gr-muted-fg)] tabular-nums min-w-[3ch] text-right\"\n :class=\"progressTextClass\"\n >{{ progressText }}</span>\n </div>\n </div>\n </slot>\n </div>\n </div>\n </div>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"GrFileUpload-C0ROpmOY.js","names":["$slots"],"sources":["../../../../src/components/GrFileUpload/fileEntry.ts","../../../../src/components/GrFileUpload/useDropZone.ts","../../../../src/components/GrFileUpload/uploadViaXhr.ts","../../../../src/components/GrFileUpload/usePerFileUpload.ts","../../../../src/components/GrFileUpload/uploadState.ts","../../../../src/components/GrFileUpload/useUploadState.ts","../../../../src/components/GrFileUpload/GrFileUpload.vue","../../../../src/components/GrFileUpload/GrFileUpload.vue"],"sourcesContent":["import type { GrUploadState } from './uploadState'\n\n/**\n * Состояние одного файла в пофайловом режиме (`uploadMode=\"per-file\"`).\n *\n * Батчевый режим по-прежнему знает только сводное `GrUploadState`: весь набор\n * уходит одним запросом, и говорить про отдельный файл там нечем.\n */\nexport type GrFileUploadStatus = 'pending' | 'uploading' | 'success' | 'error'\n\nexport interface GrFileUploadEntry {\n file: File\n status: GrFileUploadStatus\n /** Процент `0..100`. У `pending` — 0, у `success` — 100. */\n percent: number\n /** Причина ошибки, если `status === 'error'`. */\n error?: unknown\n}\n\nexport function createFileEntry(file: File): GrFileUploadEntry {\n return { file, status: 'pending', percent: 0 }\n}\n\n/**\n * Сводное состояние набора: грузимся, пока грузится хоть один; ошибка, если\n * после завершения есть хоть одна; успех — когда успешны все.\n *\n * Процент — среднее по файлам, а не по байтам: у пофайловых запросов размеры\n * разные, и «взвешенная» полоса дёргалась бы назад, когда большой файл\n * стартует после маленького.\n */\nexport function summarizeFileEntries(entries: GrFileUploadEntry[]): GrUploadState {\n if (!entries.length)\n return { phase: 'idle', percent: 0, indeterminate: false, loaded: 0, total: 0 }\n\n const total = entries.length\n const percent = Math.round(entries.reduce((sum, entry) => sum + entry.percent, 0) / total)\n const failed = entries.filter(entry => entry.status === 'error')\n const active = entries.some(entry => entry.status === 'uploading' || entry.status === 'pending')\n\n if (active) {\n return {\n phase: 'uploading',\n percent,\n // Неопределённым считаем только самое начало: пока ни один файл не\n // отчитался, полоса не должна притворяться, что что-то знает.\n indeterminate: percent === 0,\n loaded: 0,\n total: 0,\n }\n }\n\n if (failed.length) {\n return {\n phase: 'error',\n percent,\n indeterminate: false,\n loaded: 0,\n total: 0,\n error: failed[0].error,\n }\n }\n\n return { phase: 'success', percent: 100, indeterminate: false, loaded: 0, total: 0 }\n}\n","import { ref, type Ref } from 'vue'\n\nexport interface UseDropZoneOptions {\n /** Зона выключена: ввод не принимается (`disabled` или `readonly`). */\n locked: () => boolean\n /** UI рисует слот — зона перестаёт быть зоной, обработка уезжает к нему. */\n inactive: () => boolean\n onDrop: (files: File[]) => void\n}\n\nexport interface UseDropZoneReturn {\n /** Файл висит над зоной. */\n isOver: Ref<boolean>\n onDragEnter: (event: DragEvent) => void\n onDragOver: (event: DragEvent) => void\n onDragLeave: (event: DragEvent) => void\n onDropFiles: (event: DragEvent) => void\n}\n\n/**\n * Drag&drop поверх зоны.\n *\n * Счётчик входов, а не флаг: `dragenter`/`dragleave` приходят и от вложенных\n * элементов, и без счётчика подсветка гасла, стоило курсору пересечь границу\n * подписи внутри зоны.\n */\nexport function useDropZone(options: UseDropZoneOptions): UseDropZoneReturn {\n const isOver = ref(false)\n let overCounter = 0\n\n function setOver(next: boolean): void {\n if (isOver.value === next)\n return\n isOver.value = next\n }\n\n function ignored(): boolean {\n return options.inactive() || options.locked()\n }\n\n function onDragEnter(event: DragEvent): void {\n if (ignored())\n return\n\n event.preventDefault()\n overCounter += 1\n setOver(true)\n }\n\n function onDragOver(event: DragEvent): void {\n if (ignored())\n return\n\n event.preventDefault()\n if (event.dataTransfer)\n event.dataTransfer.dropEffect = 'copy'\n }\n\n function onDragLeave(event: DragEvent): void {\n if (ignored())\n return\n\n event.preventDefault()\n overCounter = Math.max(0, overCounter - 1)\n if (overCounter === 0)\n setOver(false)\n }\n\n function onDropFiles(event: DragEvent): void {\n if (ignored())\n return\n\n event.preventDefault()\n overCounter = 0\n setOver(false)\n\n const files = event.dataTransfer?.files\n ? Array.prototype.slice.call(event.dataTransfer.files) as File[]\n : []\n\n options.onDrop(files)\n }\n\n return { isOver, onDragEnter, onDragOver, onDragLeave, onDropFiles }\n}\n","/**\n * Загрузка файлов через `XMLHttpRequest` с поддержкой прогресса аплоада и\n * отмены по `AbortSignal`. Используется в `GrFileUpload` для ветки `action`,\n * а также может применяться пользователем напрямую в `request`-загрузчике.\n */\n\nexport type GrUploadExtraDataValue = string | Blob\n\nexport type GrUploadExtraData = Record<string, GrUploadExtraDataValue | GrUploadExtraDataValue[]>\n\nexport interface GrUploadProgressInfo {\n /** Процент `0..100`. Для запросов без `lengthComputable` значение `0` и `indeterminate = true`. */\n percent: number\n loaded: number\n total: number\n indeterminate: boolean\n}\n\nexport interface UploadViaXhrOptions {\n url: string\n files: File[]\n name: string\n headers?: Record<string, string>\n withCredentials?: boolean\n extraData?: GrUploadExtraData\n signal: AbortSignal\n onProgress?: (info: GrUploadProgressInfo) => void\n}\n\nexport class GrUploadAbortError extends Error {\n constructor(message = 'Upload aborted') {\n super(message)\n this.name = 'GrUploadAbortError'\n }\n}\n\nexport class GrUploadHttpError extends Error {\n status: number\n payload: unknown\n\n constructor(status: number, payload: unknown) {\n super(`Upload failed with status ${status}`)\n this.name = 'GrUploadHttpError'\n this.status = status\n this.payload = payload\n }\n}\n\nfunction appendExtraFormData(body: FormData, extraData: GrUploadExtraData | undefined) {\n if (!extraData)\n return\n\n for (const [key, value] of Object.entries(extraData)) {\n if (Array.isArray(value)) {\n for (const entry of value) body.append(key, entry)\n continue\n }\n\n body.append(key, value)\n }\n}\n\nfunction parseResponse(xhr: XMLHttpRequest): unknown {\n const contentType = xhr.getResponseHeader('content-type') ?? ''\n const raw = xhr.responseText\n\n if (contentType.includes('application/json')) {\n try {\n return JSON.parse(raw)\n }\n catch {\n return raw\n }\n }\n\n return raw\n}\n\nexport function uploadViaXhr(options: UploadViaXhrOptions): Promise<unknown> {\n const { url, files, name, headers, withCredentials, extraData, signal, onProgress } = options\n\n return new Promise<unknown>((resolve, reject) => {\n if (signal.aborted) {\n reject(new GrUploadAbortError())\n return\n }\n\n const body = new FormData()\n for (const file of files) body.append(name, file)\n appendExtraFormData(body, extraData)\n\n const xhr = new XMLHttpRequest()\n xhr.open('POST', url, true)\n xhr.withCredentials = !!withCredentials\n\n if (headers) {\n for (const [key, value] of Object.entries(headers))\n xhr.setRequestHeader(key, value)\n }\n\n const onAbort = () => {\n try {\n xhr.abort()\n }\n catch {\n /* ignore */\n }\n }\n\n signal.addEventListener('abort', onAbort)\n\n const cleanup = () => {\n signal.removeEventListener('abort', onAbort)\n }\n\n if (onProgress) {\n xhr.upload.addEventListener('progress', (event) => {\n const indeterminate = !event.lengthComputable\n const total = indeterminate ? 0 : event.total\n const loaded = indeterminate ? 0 : event.loaded\n const percent = indeterminate || total === 0 ? 0 : Math.min(100, (loaded / total) * 100)\n onProgress({ percent, loaded, total, indeterminate })\n })\n }\n\n xhr.addEventListener('load', () => {\n cleanup()\n const payload = parseResponse(xhr)\n const status = xhr.status\n\n if (status >= 200 && status < 300) {\n if (onProgress)\n onProgress({ percent: 100, loaded: 1, total: 1, indeterminate: false })\n resolve(payload)\n return\n }\n\n reject(new GrUploadHttpError(status, payload))\n })\n\n xhr.addEventListener('error', () => {\n cleanup()\n reject(new Error('Network error during upload'))\n })\n\n xhr.addEventListener('abort', () => {\n cleanup()\n reject(new GrUploadAbortError())\n })\n\n xhr.send(body)\n })\n}\n","import { ref, type Ref } from 'vue'\n\nimport { createFileEntry, summarizeFileEntries, type GrFileUploadEntry } from './fileEntry'\nimport type { GrUploadState } from './uploadState'\nimport { GrUploadAbortError, type GrUploadProgressInfo } from './uploadViaXhr'\n\nexport interface UsePerFileUploadOptions {\n /** Сколько файлов грузится одновременно. */\n concurrency: () => number\n /** Отправка одного файла. Контракт тот же, что у батча: всегда массив. */\n send: (files: File[], ctx: { signal: AbortSignal, onProgress: (info: GrUploadProgressInfo) => void }) => Promise<unknown>\n /** Сводное состояние пересчитывается из записей по файлам. */\n onSummary: (state: GrUploadState) => void\n onProgress: (info: GrUploadProgressInfo, file: File) => void\n onSuccess: (payload: unknown, file: File) => void\n onError: (error: unknown, file: File) => void\n onSetChange: (files: File[]) => void\n}\n\nexport interface UsePerFileUploadReturn {\n entries: Ref<GrFileUploadEntry[]>\n entryOf: (file: File) => GrFileUploadEntry | undefined\n /** Загрузить набор пофайлово: у каждого файла свой запрос и свой статус. */\n run: (files: File[]) => Promise<void>\n /** Повторить один файл — соседей это не касается. */\n retryFile: (file: File) => Promise<void>\n /** Оборвать загрузку одного файла. */\n abortFile: (file: File) => void\n abortAll: () => void\n /** Убрать файл из набора записей и пересчитать сводное состояние. */\n dropEntry: (file: File) => void\n reset: () => void\n}\n\n/**\n * Пофайловый режим: у каждого файла свой запрос, свой контроллер и свой статус,\n * а сводное состояние выводится из них.\n */\nexport function usePerFileUpload(options: UsePerFileUploadOptions): UsePerFileUploadReturn {\n const entries = ref<GrFileUploadEntry[]>([])\n\n /** Контроллер на файл: `abortFile` обязан обрывать свой запрос, а не соседний. */\n const controllers = new Map<File, AbortController>()\n\n function syncSummary(): void {\n options.onSummary(summarizeFileEntries(entries.value))\n }\n\n function entryOf(file: File): GrFileUploadEntry | undefined {\n return entries.value.find(entry => entry.file === file)\n }\n\n function patch(file: File, next: Partial<GrFileUploadEntry>): void {\n const entry = entryOf(file)\n if (!entry)\n return\n\n Object.assign(entry, next)\n syncSummary()\n }\n\n async function uploadOne(file: File): Promise<void> {\n const controller = new AbortController()\n controllers.set(file, controller)\n patch(file, { status: 'uploading', percent: 0, error: undefined })\n\n const onProgress = (info: GrUploadProgressInfo): void => {\n patch(file, { percent: info.indeterminate ? 0 : info.percent })\n options.onProgress(info, file)\n }\n\n try {\n const payload = await options.send([file], { signal: controller.signal, onProgress })\n\n patch(file, { status: 'success', percent: 100, error: undefined })\n options.onSuccess(payload, file)\n options.onProgress({ percent: 100, loaded: 0, total: 0, indeterminate: false }, file)\n }\n catch (error) {\n // Отмена — не ошибка файла: строка возвращается в исходное состояние, а\n // пользователь решает, повторять её или убрать.\n if (error instanceof GrUploadAbortError || (error as { name?: string })?.name === 'AbortError')\n patch(file, { status: 'pending', percent: 0, error: undefined })\n else\n patch(file, { status: 'error', error })\n\n options.onError(error, file)\n }\n finally {\n controllers.delete(file)\n }\n }\n\n /**\n * Очередь с ограничением параллелизма. Без неё «пофайлово» означало бы «все\n * сразу»: сотня файлов открыла бы сотню соединений.\n */\n async function runWithConcurrency(files: File[]): Promise<void> {\n const limit = Math.max(1, Math.floor(options.concurrency()))\n const queue = [...files]\n\n const workers = Array.from({ length: Math.min(limit, queue.length) }, async () => {\n while (queue.length) {\n const next = queue.shift()\n if (!next)\n return\n await uploadOne(next)\n }\n })\n\n await Promise.all(workers)\n }\n\n async function run(files: File[]): Promise<void> {\n entries.value = files.map(createFileEntry)\n syncSummary()\n\n await runWithConcurrency(files)\n options.onSetChange(files)\n }\n\n async function retryFile(file: File): Promise<void> {\n if (!entryOf(file))\n return\n\n await uploadOne(file)\n }\n\n function abortFile(file: File): void {\n controllers.get(file)?.abort()\n controllers.delete(file)\n }\n\n function abortAll(): void {\n for (const controller of controllers.values()) controller.abort()\n controllers.clear()\n }\n\n function dropEntry(file: File): void {\n entries.value = entries.value.filter(entry => entry.file !== file)\n }\n\n function reset(): void {\n entries.value = []\n }\n\n return { entries, entryOf, run, retryFile, abortFile, abortAll, dropEntry, reset }\n}\n","/**\n * Состояние процесса загрузки в `GrFileUpload`.\n *\n * Используется как для внутреннего управления UI, так и пробрасывается наружу\n * через scoped-слоты (`progress`, `default`) и `defineExpose`.\n */\n\nexport type GrUploadPhase = 'idle' | 'uploading' | 'success' | 'error'\n\nexport interface GrUploadStateIdle {\n phase: 'idle'\n percent: 0\n indeterminate: false\n loaded: 0\n total: 0\n}\n\nexport interface GrUploadStateUploading {\n phase: 'uploading'\n percent: number\n indeterminate: boolean\n loaded: number\n total: number\n}\n\nexport interface GrUploadStateSuccess {\n phase: 'success'\n percent: 100\n indeterminate: false\n loaded: number\n total: number\n}\n\nexport interface GrUploadStateError {\n phase: 'error'\n percent: number\n indeterminate: false\n loaded: number\n total: number\n error: unknown\n}\n\nexport type GrUploadState\n = | GrUploadStateIdle\n | GrUploadStateUploading\n | GrUploadStateSuccess\n | GrUploadStateError\n\nexport const GR_UPLOAD_STATE_IDLE: GrUploadStateIdle = Object.freeze({\n phase: 'idle',\n percent: 0,\n indeterminate: false,\n loaded: 0,\n total: 0,\n})\n","import { reactive } from 'vue'\n\nimport { GR_UPLOAD_STATE_IDLE, type GrUploadState } from './uploadState'\nimport type { GrUploadProgressInfo } from './uploadViaXhr'\n\nexport interface UseUploadStateOptions {\n /** Компонент отчитывается о каждой смене фазы наружу. */\n onChange: (state: GrUploadState) => void\n}\n\nexport interface UseUploadStateReturn {\n state: GrUploadState\n setIdle: () => void\n setUploading: (info?: GrUploadProgressInfo) => void\n setSuccess: (info: { loaded: number, total: number }) => void\n setError: (error: unknown) => void\n /** Прогресс не двигает состояние вне фазы `uploading`. */\n applyProgress: (info: GrUploadProgressInfo) => void\n assign: (next: GrUploadState) => void\n /** Отложенный переход `success` → `idle`. */\n scheduleIdle: (delayMs: number) => void\n clearScheduledIdle: () => void\n}\n\n/**\n * Сводное состояние загрузки: фаза, проценты и таймер скрытия успеха.\n *\n * Состояние — `reactive`-объект с меняющейся формой (у `error` есть поле\n * `error`, у `idle` его нет), поэтому при смене фазы старые ключи удаляются:\n * иначе `phase: 'idle'` тащил бы за собой `error` от прошлой попытки.\n */\nexport function useUploadState(options: UseUploadStateOptions): UseUploadStateReturn {\n const state = reactive<GrUploadState>({ ...GR_UPLOAD_STATE_IDLE }) as GrUploadState\n\n let idleTimer: ReturnType<typeof setTimeout> | null = null\n\n function clearScheduledIdle(): void {\n if (idleTimer === null)\n return\n\n clearTimeout(idleTimer)\n idleTimer = null\n }\n\n function assign(next: GrUploadState): void {\n // Форма состояния меняется вместе с фазой (`error` есть только у ошибки),\n // поэтому старые ключи снимаются: иначе `idle` тащил бы `error` прошлой\n // попытки.\n for (const key of Object.keys(state))\n delete (state as unknown as Record<string, unknown>)[key]\n\n Object.assign(state, next)\n options.onChange(state)\n }\n\n function setIdle(): void {\n assign({ ...GR_UPLOAD_STATE_IDLE })\n }\n\n function setUploading(info?: GrUploadProgressInfo): void {\n assign({\n phase: 'uploading',\n percent: info?.percent ?? 0,\n indeterminate: info?.indeterminate ?? true,\n loaded: info?.loaded ?? 0,\n total: info?.total ?? 0,\n })\n }\n\n function setSuccess(info: { loaded: number, total: number }): void {\n assign({\n phase: 'success',\n percent: 100,\n indeterminate: false,\n loaded: info.loaded,\n total: info.total,\n })\n }\n\n function setError(error: unknown): void {\n const prev = state\n assign({\n phase: 'error',\n percent: prev.phase === 'uploading' ? prev.percent : 0,\n indeterminate: false,\n loaded: prev.phase === 'uploading' ? prev.loaded : 0,\n total: prev.phase === 'uploading' ? prev.total : 0,\n error,\n })\n }\n\n function applyProgress(info: GrUploadProgressInfo): void {\n if (state.phase !== 'uploading')\n return\n\n assign({\n phase: 'uploading',\n percent: info.percent,\n indeterminate: info.indeterminate,\n loaded: info.loaded,\n total: info.total,\n })\n }\n\n function scheduleIdle(delayMs: number): void {\n clearScheduledIdle()\n idleTimer = setTimeout(() => {\n if (state.phase === 'success')\n setIdle()\n idleTimer = null\n }, delayMs)\n }\n\n return {\n state,\n setIdle,\n setUploading,\n setSuccess,\n setError,\n applyProgress,\n assign,\n scheduleIdle,\n clearScheduledIdle,\n }\n}\n","<script setup lang=\"ts\" generic=\"TResponse = unknown\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, Text, useSlots, watch, type VNode } from 'vue'\n\nimport IconArrowUp from '~icons/lucide/arrow-up'\nimport IconClose from '~icons/lucide/x'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport GrProgressBar from '../GrProgressBar/GrProgressBar.vue'\nimport type { GrProgressBarTone } from '../GrProgressBar/grStyle'\nimport type { FileValidator, FileValidatorSource } from '../../fileValidation'\nimport type { GrUploadProgressInfo } from './uploadViaXhr'\nimport type { GrUploadState } from './uploadState'\nimport { summarizeFileEntries, type GrFileUploadEntry } from './fileEntry'\nimport { useDropZone } from './useDropZone'\nimport { useFilePreviews } from '../../composables/internal/useFilePreviews'\nimport { usePerFileUpload } from './usePerFileUpload'\nimport { useUploadState } from './useUploadState'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { flattenSlotNodes, meaningfulSlotNodes } from '../shared/slotNodes'\nimport {\n type GrFileUploadSize,\n grFileUploadZoneClass,\n hintSizes,\n iconGlyphSizes,\n iconTileSizes,\n labelSizes,\n progressBarSizes,\n progressTextSizes,\n zoneGaps,\n} from './grFileUploadStyles'\nimport { acceptValidator, FileValidationError, runFileValidators } from '../../fileValidation'\nimport { GrUploadAbortError, uploadViaXhr } from './uploadViaXhr'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nexport type GrFileUploadMode = 'batch' | 'per-file'\n\nexport type GrFileUploadExtraDataValue = string | Blob\n\nexport type GrFileUploadExtraData = Record<string, GrFileUploadExtraDataValue | GrFileUploadExtraDataValue[]>\n\nexport type GrFileUploadRequestCtx = {\n signal: AbortSignal\n extraData?: GrFileUploadExtraData\n /**\n * Колбэк прогресса аплоада. Пользовательский `request` (например, axios)\n * должен вызывать его из `onUploadProgress`, чтобы `GrFileUpload` отображал\n * прогресс через слот `progress` / дефолтный `GrProgressBar`.\n */\n onProgress?: (info: GrUploadProgressInfo) => void\n}\n\nexport type GrFileUploadRequest<TResponse = unknown> = (\n files: File[],\n ctx: GrFileUploadRequestCtx,\n) => Promise<TResponse>\n\n/**\n * Пропы `GrFileUpload`.\n *\n * Либо `action` (URL для POST multipart/form-data), либо `request` — кастомный\n * загрузчик (например, через axios). Если переданы оба — приоритет у `request`.\n *\n * `placeholder` — надпись в дефолтном UI-варианте (без слота default).\n */\nexport interface GrFileUploadProps<TResponse = unknown> {\n /**\n * Набор файлов — тот, что показан в списке и уйдёт в `retry`.\n *\n * Проп **необязателен**: без него компонент держит набор сам. Передан —\n * набор следует за ним, и потребитель может очистить или подменить список\n * снаружи.\n */\n modelValue?: File[]\n action?: string\n request?: GrFileUploadRequest<TResponse>\n name?: string\n multiple?: boolean\n limit?: number\n /**\n * Guard перед отправкой. Остаётся пропом, а не эмитом, осознанно: эмит не\n * возвращает значения, а этот колбэк обязан ответить «пускать или нет».\n * Уведомления — `exceed`, `success`, `error`, `progress` — эмиты.\n */\n beforeUpload?: (file: File) => boolean | Promise<unknown>\n validators?: FileValidator[]\n /**\n * W3C `accept` для `<input type=\"file\">` — и sugar к `acceptValidator(...)`.\n *\n * Только атрибута мало: он фильтрует системный диалог, но не drag&drop —\n * перетащить можно что угодно. Поэтому то же значение уходит и в валидаторы,\n * как в `GrFormFile`.\n */\n accept?: string\n /** `capture` для мобильной камеры/микрофона. */\n capture?: 'user' | 'environment'\n /** Выбор каталога целиком (`webkitdirectory`). Поддержка — Chromium и Safari. */\n directory?: boolean\n disabled?: boolean\n /** Только для чтения: значение видно и уходит в форму, но не редактируется. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n headers?: Record<string, string>\n withCredentials?: boolean\n showFileList?: boolean\n uploadExtraData?: (files: File[]) => GrFileUploadExtraData | undefined\n /** i18n: надпись-подсказка в дефолтном UI. */\n placeholder?: string\n /** Показывать дефолтный прогресс-бар (если не используется слот `progress`). */\n showProgress?: boolean\n /** Цветовой тон полосы прогресса в фазе `uploading`. */\n progressTone?: GrProgressBarTone\n /** aria-label для прогресс-бара. */\n progressLabel?: string\n /** Через сколько мс после `success` скрыть прогресс-бар. `0` — не скрывать. */\n hideProgressOnSuccess?: number\n /** Размер дефолтного UI: поля дроп-зоны, плитка иконки, кегль подписей. */\n size?: GrFileUploadSize\n /**\n * Как уходит набор файлов:\n *\n * - `batch` (по умолчанию) — весь набор одним запросом. Про отдельный файл\n * сказать нечего, поэтому и статуса у него нет;\n * - `per-file` — на каждый файл свой запрос (`request` зовётся с массивом из\n * одного файла, контракт не меняется). Появляются статус и процент строки,\n * `retryFile` и `abortFile`.\n */\n uploadMode?: GrFileUploadMode\n /** Сколько файлов грузится одновременно в режиме `per-file`. */\n concurrency?: number\n /** Миниатюры для `image/*` в списке файлов. */\n preview?: boolean\n}\n\nexport interface GrFileUploadEmits<TResponse = unknown> {\n /** Набор файлов сменился: новый выбор или удаление. Не путать с `change`. */\n (e: 'update:modelValue', files: File[]): void\n /** Выбрано больше файлов, чем разрешает `limit`. Загрузка не стартует. */\n (e: 'exceed', files: File[], limit: number): void\n /** `file` приходит только в режиме `per-file`: в батче отчитываться нечем. */\n (e: 'success', payload: TResponse, file?: File): void\n (e: 'error', error: unknown, file?: File): void\n (e: 'progress', percent: number, info?: GrUploadProgressInfo, file?: File): void\n (e: 'change', files: File[]): void\n (e: 'stateChange', state: GrUploadState): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrFileUploadProps<TResponse>>(),\n {\n modelValue: undefined,\n action: undefined,\n request: undefined,\n name: 'file',\n multiple: false,\n limit: undefined,\n beforeUpload: undefined,\n validators: undefined,\n accept: undefined,\n capture: undefined,\n directory: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n headers: undefined,\n withCredentials: false,\n showFileList: false,\n uploadExtraData: undefined,\n placeholder: undefined,\n showProgress: true,\n progressTone: 'primary',\n progressLabel: undefined,\n hideProgressOnSuccess: 800,\n size: undefined,\n uploadMode: 'batch',\n concurrency: 3,\n preview: false,\n },\n)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFileUpload' })\n\nconst zoneGapClass = computed(() => zoneGaps[resolvedSize.value])\nconst iconTileClass = computed(() => iconTileSizes[resolvedSize.value])\nconst iconGlyphSize = computed(() => iconGlyphSizes[resolvedSize.value])\nconst labelClass = computed(() => labelSizes[resolvedSize.value])\nconst hintClass = computed(() => hintSizes[resolvedSize.value])\nconst progressTextClass = computed(() => progressTextSizes[resolvedSize.value])\nconst progressBarSize = computed(() => progressBarSizes[resolvedSize.value])\n\nconst emit = defineEmits<GrFileUploadEmits<TResponse>>()\ndefineSlots<{\n /** Полностью своя зона загрузки: контрол отдаёт наружу всё своё состояние. */\n default?: (props: {\n openDialog: () => void\n abort: () => void\n disabled: boolean\n files: File[]\n isOver: boolean\n state: GrUploadState\n retry: () => Promise<void>\n removeFile: (file: File) => void\n fileEntries: GrFileUploadEntry[]\n retryFile: (file: File) => Promise<void>\n abortFile: (file: File) => void\n }) => any\n /** Заголовок зоны вместо `placeholder`. */\n label?: () => any\n /** Подпись под заголовком: ограничения по типу и размеру. */\n tip?: () => any\n /** Свой индикатор прогресса вместо встроенного. */\n progress?: (props: {\n state: GrUploadState\n percent: number\n indeterminate: boolean\n phase: GrUploadState['phase']\n files: File[]\n abort: () => void\n retry: () => Promise<void>\n fileEntries: GrFileUploadEntry[]\n retryFile: (file: File) => Promise<void>\n abortFile: (file: File) => void\n }) => any\n}>()\n\nconst slots = useSlots()\n\nconst { t } = useGranularityTranslations()\n\n// Контекст `GrFormField`: доступным контролом служит сам нативный\n// `<input type=\"file\">` — он и остаётся целью `<label for>`.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n readonly: isReadonly,\n invalid: isInvalid,\n required: isRequired,\n // `locked` — «ввод не принимается»: `disabled` или `readonly`. Набор в\n // `readonly` виден и уходит в форму, но поменять его нельзя.\n locked: isLocked,\n} = useGrFormControl(() => props)\nconst resolvedPlaceholder = computed(() => props.placeholder ?? t('gr.fileUpload.placeholder', 'Drag files here or click to select'))\nconst resolvedProgressLabel = computed(() => props.progressLabel ?? t('gr.fileUpload.progress', 'Upload progress'))\n\nfunction slotIsTextOnly(nodes: VNode[]): boolean {\n const meaningful = meaningfulSlotNodes(nodes)\n\n if (meaningful.length === 0)\n return true\n\n for (const node of meaningful) {\n if (node.type !== Text)\n return false\n }\n\n return true\n}\n\nconst defaultSlotNodes = computed(() => {\n return slots.default ? flattenSlotNodes(slots.default({} as any)) : []\n})\n\nconst hasCustomUi = computed(() => {\n return !!slots.default && !slotIsTextOnly(defaultSlotNodes.value)\n})\n\nconst inputRef = ref<HTMLInputElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Внутри зоны фокус ходит между нативным input'ом и кнопками списка файлов:\n// без границы каждое перемещение давало бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n\nconst lastFiles = ref<File[]>(props.modelValue ? [...props.modelValue] : [])\n\n/**\n * Набор меняется только здесь: иначе `update:modelValue` рано или поздно\n * разошёлся бы с тем, что показано в списке.\n */\nfunction setFiles(next: File[]): void {\n lastFiles.value = next\n emit('update:modelValue', next)\n}\n\n// Контролируемый режим: проп задан — он и есть источник истины.\nwatch(() => props.modelValue, (next) => {\n if (next === undefined)\n return\n if (next === lastFiles.value)\n return\n\n lastFiles.value = [...next]\n}, { deep: false })\n\nconst {\n state,\n setIdle: setStateIdle,\n setUploading: setStateUploading,\n setSuccess: setStateSuccess,\n setError: setStateError,\n applyProgress,\n assign: assignState,\n scheduleIdle,\n clearScheduledIdle,\n} = useUploadState({ onChange: next => emit('stateChange', next) })\n\nlet activeController: AbortController | null = null\n\nfunction handleProgress(info: GrUploadProgressInfo) {\n applyProgress(info)\n emit('progress', info.percent, info)\n}\n\nconst { fileKey, previewUrl, revokePreview, revokeAllPreviews } = useFilePreviews({\n enabled: () => props.preview,\n})\n\n/**\n * `extraData` считается один раз на набор и не меняется между файлами, поэтому\n * живёт рядом с очередью, а не путешествует через её контракт.\n */\nlet perFileExtraData: GrFileUploadExtraData | undefined\n\nconst perFile = usePerFileUpload({\n concurrency: () => props.concurrency,\n send: (files, ctx) => (props.request\n ? props.request(files, { signal: ctx.signal, extraData: perFileExtraData, onProgress: ctx.onProgress })\n : uploadViaXhr({\n url: props.action ?? '',\n files,\n name: props.name,\n headers: props.headers,\n withCredentials: props.withCredentials,\n extraData: perFileExtraData,\n signal: ctx.signal,\n onProgress: ctx.onProgress,\n })),\n onSummary: assignState,\n onProgress: (info, file) => emit('progress', info.percent, info, file),\n onSuccess: (payload, file) => emit('success', payload as TResponse, file),\n onError: (error, file) => emit('error', error, file),\n onSetChange: files => emit('change', files),\n})\n\nconst fileEntries = perFile.entries\nconst { retryFile, abortFile } = perFile\n\nconst dropZone = useDropZone({\n locked: () => isLocked.value,\n inactive: () => hasCustomUi.value,\n onDrop: files => void handleFiles(files, 'drop'),\n})\nconst isOver = dropZone.isOver\n\n/**\n * Дефолтный слот вызывается из двух мест — как своя зона целиком и как подпись\n * внутри стандартной. Набор пропов у него один: две копии разъехались бы.\n */\nconst defaultSlotProps = computed(() => ({\n openDialog,\n abort,\n disabled: isDisabled.value,\n files: lastFiles.value,\n isOver: isOver.value,\n state,\n retry,\n removeFile,\n fileEntries: fileEntries.value,\n retryFile,\n abortFile,\n}))\n\nconst zoneClass = computed(() => grFileUploadZoneClass({\n size: resolvedSize.value,\n disabled: isDisabled.value,\n readonly: isReadonly.value,\n over: isOver.value,\n}))\n\nconst hiddenInputStyle = {\n position: 'fixed',\n top: '-9999px',\n left: '-9999px',\n width: '1px',\n height: '1px',\n opacity: '0',\n overflow: 'hidden',\n pointerEvents: 'none',\n} as const satisfies Record<string, string>\n\nfunction openDialog() {\n if (isLocked.value)\n return\n inputRef.value?.click()\n}\n\nfunction abort() {\n activeController?.abort()\n activeController = null\n}\n\nfunction totalSizeOf(files: File[]): number {\n return files.reduce((sum, file) => sum + file.size, 0)\n}\n\nfunction normalizeLimit(limit: number | undefined): number | undefined {\n if (typeof limit !== 'number')\n return undefined\n if (!Number.isFinite(limit))\n return undefined\n if (limit <= 0)\n return undefined\n return Math.floor(limit)\n}\n\nasync function uploadViaAction(files: File[], signal: AbortSignal, extraData: GrFileUploadExtraData | undefined) {\n if (!props.action)\n throw new Error('GrFileUpload: either `action` or `request` must be provided')\n\n return uploadViaXhr({\n url: props.action,\n files,\n name: props.name,\n headers: props.headers,\n withCredentials: props.withCredentials,\n extraData,\n signal,\n onProgress: handleProgress,\n })\n}\n\nasync function runBeforeUpload(files: File[]): Promise<'ok' | { aborted: true, reason: unknown }> {\n if (!props.beforeUpload)\n return 'ok'\n\n for (const file of files) {\n try {\n const result = props.beforeUpload(file)\n\n if (result === false) {\n return { aborted: true, reason: new Error('Upload aborted by beforeUpload') }\n }\n\n if (result && typeof (result as any).then === 'function')\n await result\n }\n catch (error) {\n return { aborted: true, reason: error }\n }\n }\n\n return 'ok'\n}\n\n/**\n * `accept` идёт первым валидатором: атрибут на input фильтрует только диалог,\n * а drop мимо него проходит.\n */\nfunction effectiveValidators(): FileValidator[] {\n return [acceptValidator(props.accept), ...(props.validators ?? [])]\n}\n\n/**\n * Номер запуска. Валидация и `beforeUpload` асинхронны, поэтому два быстрых\n * выбора подряд идут внахлёст: без номера «побеждал» тот, чьи валидаторы\n * отработали позже, — он же обрывал уже стартовавшую загрузку соседа.\n * Актуален всегда последний выбор, остальные тихо сходят с дистанции.\n */\nlet runCounter = 0\n\nasync function handleFiles(files: File[], source: FileValidatorSource = 'input') {\n if (isLocked.value)\n return\n if (!files.length)\n return\n\n runCounter += 1\n const runId = runCounter\n const isStale = (): boolean => runId !== runCounter\n\n const normalizedLimit = normalizeLimit(props.limit)\n if (props.multiple && normalizedLimit && files.length > normalizedLimit) {\n emit('exceed', files, normalizedLimit)\n emit('error', new Error(`Too many files selected, limit=${normalizedLimit}`))\n return\n }\n\n const { files: valid, issues } = await runFileValidators(files, effectiveValidators(), {\n source,\n multiple: props.multiple,\n })\n\n if (isStale())\n return\n\n if (issues.length > 0) {\n emit('error', new FileValidationError(issues, valid))\n return\n }\n\n if (!valid.length)\n return\n\n // Набор сменился — миниатюры прежнего больше не нужны.\n revokeAllPreviews()\n setFiles(valid)\n\n const before = await runBeforeUpload(valid)\n if (isStale())\n return\n\n if (before !== 'ok') {\n emit('error', before.reason)\n return\n }\n\n let extraData: GrFileUploadExtraData | undefined\n try {\n extraData = props.uploadExtraData?.(valid)\n }\n catch (error) {\n emit('error', error)\n return\n }\n\n abort()\n clearScheduledIdle()\n\n if (props.uploadMode === 'per-file') {\n perFileExtraData = extraData\n await perFile.run(valid)\n return\n }\n\n const controller = new AbortController()\n activeController = controller\n\n // Батч: пофайловых записей нет, показываем набор без статусов.\n perFile.reset()\n\n setStateUploading()\n emit('progress', 0, { percent: 0, loaded: 0, total: 0, indeterminate: true })\n\n try {\n // `action`-ветка отдаёт разобранный ответ сервера: типом его знает только\n // потребитель, поэтому приводим к его же `TResponse`.\n const payload = (props.request\n ? await props.request(valid, { signal: controller.signal, extraData, onProgress: handleProgress })\n : await uploadViaAction(valid, controller.signal, extraData)) as TResponse\n\n // Кастомный `request` не обязан звать `onProgress` — тогда байты неизвестны.\n // Сумма размеров выбранных файлов честнее нуля: «100%» при `total: 0`\n // потребитель прочитает как «загружено ноль».\n const measured = state.phase === 'uploading' ? state.total || state.loaded : 0\n const finalLoaded = measured || totalSizeOf(valid)\n setStateSuccess({ loaded: finalLoaded, total: finalLoaded })\n emit('success', payload)\n emit('change', valid)\n emit('progress', 100, { percent: 100, loaded: finalLoaded, total: finalLoaded, indeterminate: false })\n\n if (props.hideProgressOnSuccess && props.hideProgressOnSuccess > 0)\n scheduleIdle(props.hideProgressOnSuccess)\n }\n catch (error) {\n if (error instanceof GrUploadAbortError) {\n setStateIdle()\n }\n else {\n setStateError(error)\n }\n emit('error', error)\n }\n finally {\n if (activeController === controller)\n activeController = null\n }\n}\n\n/**\n * `<input type=\"file\">` не понимает `readonly` — атрибута для него в HTML нет.\n * Поэтому системный диалог гасим отменой действия по умолчанию: input остаётся\n * в порядке `Tab` и объявляется как «только чтение», но нового файла не примет.\n */\nfunction onInputClick(event: MouseEvent) {\n if (isReadonly.value && !isDisabled.value)\n event.preventDefault()\n}\n\nasync function onInputChange(event: Event) {\n if (isLocked.value)\n return\n\n const target = event.target as HTMLInputElement | null\n const files = target?.files ? Array.prototype.slice.call(target.files) as File[] : []\n\n if (target)\n target.value = ''\n\n await nextTick()\n await handleFiles(files, 'input')\n}\n\n// Клавиатуры на зоне нет: Enter/Space обрабатывает сам file-input, на котором и\n// стоит фокус. Собственный обработчик здесь только дублировал бы нативный и\n// открывал диалог дважды.\nfunction onRootClick(event: MouseEvent) {\n if (hasCustomUi.value)\n return\n // `openDialog` кликает по input программно, и этот клик всплывает обратно сюда.\n // Без отсечки зона открывала бы диалог по кругу.\n if (event.target === inputRef.value)\n return\n\n openDialog()\n}\n\nconst effectiveProgressTone = computed<GrProgressBarTone>(() => {\n if (state.phase === 'error')\n return 'danger'\n if (state.phase === 'success')\n return 'success'\n return props.progressTone\n})\n\n/**\n * Живой регион существует с первого рендера и пуст, пока объявлять нечего:\n * регион, появляющийся сразу с текстом, часть AT не объявляет вовсе. Прогресс\n * в процентах сюда не идёт — диктор захлебнётся; объявляются только фазы.\n */\nconst liveMessage = computed(() => {\n if (state.phase === 'uploading')\n return t('gr.fileUpload.uploading', 'Uploading…')\n if (state.phase === 'success')\n return t('gr.fileUpload.success', 'Upload complete')\n if (state.phase === 'error')\n return t('gr.fileUpload.error', 'Upload failed')\n return ''\n})\n\nconst progressVisible = computed(() => state.phase !== 'idle')\nconst progressIndeterminate = computed(() => state.phase === 'uploading' && state.indeterminate)\nconst progressPercent = computed(() => (progressIndeterminate.value ? 0 : state.percent))\nconst progressText = computed(() => {\n if (progressIndeterminate.value)\n return ''\n return `${Math.round(progressPercent.value)}%`\n})\n\nconst isPerFile = computed(() => props.uploadMode === 'per-file')\n\nconst statusTextByStatus = computed<Record<string, string>>(() => ({\n pending: t('gr.fileUpload.statusPending', 'Pending'),\n uploading: t('gr.fileUpload.statusUploading', 'Uploading'),\n success: t('gr.fileUpload.statusSuccess', 'Uploaded'),\n error: t('gr.fileUpload.statusError', 'Failed'),\n}))\n\n/** Запись файла для разметки: в батчевом режиме её нет — и статуса тоже. */\nfunction entryFor(file: File): GrFileUploadEntry | undefined {\n return isPerFile.value ? perFile.entryOf(file) : undefined\n}\n\n/** Повторить загрузку текущего набора — после ошибки выбирать файлы заново незачем. */\nasync function retry(): Promise<void> {\n if (!lastFiles.value.length)\n return\n\n await handleFiles([...lastFiles.value], 'input')\n}\n\n/**\n * Убрать файл из набора. Идущая загрузка при этом обрывается: она была про\n * прежний набор, и её результат к новому отношения не имеет.\n */\nfunction removeFile(file: File): void {\n const next = lastFiles.value.filter(item => item !== file)\n if (next.length === lastFiles.value.length)\n return\n\n abortFile(file)\n abort()\n clearScheduledIdle()\n revokePreview(file)\n setFiles(next)\n perFile.dropEntry(file)\n\n // В пофайловом режиме остальные файлы своих статусов не теряют: удалили один\n // — сводное состояние просто пересчитывается по оставшимся.\n if (props.uploadMode === 'per-file' && fileEntries.value.length)\n assignState(summarizeFileEntries(fileEntries.value))\n else if (state.phase !== 'idle')\n setStateIdle()\n}\n\nfunction focus(): void {\n inputRef.value?.focus()\n}\n\nfunction blur(): void {\n inputRef.value?.blur()\n}\n\n// Ни `action`, ни `request` — загружать некуда, и узнать об этом на первом же\n// выборе файла поздно. Выразить требование типом нельзя: `defineProps` в SFC\n// принимает объектный тип или интерфейс, но не discriminated union.\nonMounted(() => {\n if (!__GR_DEV__)\n return\n if (props.action || props.request)\n return\n\n console.warn('[GrFileUpload] не задан ни `action`, ни `request`: отправлять файлы некуда.')\n})\n\n// Хвосты, переживающие размонтирование: незавершённый XHR продолжает качать\n// файл, а таймер скрытия успеха дёргает `setStateIdle()` на уничтоженном\n// инстансе. Сценарий обычный — успешная загрузка и уход со страницы сразу после.\nonBeforeUnmount(() => {\n clearScheduledIdle()\n abort()\n perFile.abortAll()\n revokeAllPreviews()\n})\n\ndefineExpose({\n focus,\n blur,\n uploadFiles: handleFiles,\n abort,\n openDialog,\n state,\n /** Текущий набор файлов — тот, что показан в списке и уйдёт в `retry`. */\n files: lastFiles,\n retry,\n removeFile,\n /** Записи по файлам — пусты в батчевом режиме. */\n fileEntries,\n retryFile,\n abortFile,\n})\n\n/**\n * Drop-zone намеренно БЕЗ `role=\"button\"` и `tabindex`: доступным контролом\n * служит сам нативный `<input type=\"file\">`. Роль-виджет объявляет потомков\n * презентационными, поэтому input внутри неё теряется для скринридеров и\n * падает в axe на `nested-interactive`; а обёртывать его в собственный\n * «button» — значит вручную переизобретать то, что file-input уже умеет\n * нативно (открытие диалога с клавиатуры, объявление «кнопка выбора файла»).\n * Зона остаётся кликабельной и принимает drag&drop, фокус input'а показывает\n * через `focus-within`.\n */\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-file-upload\n :class=\"hasCustomUi ? 'inline-block' : zoneClass\"\n @click=\"onRootClick\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n @dragenter=\"dropZone.onDragEnter\"\n @dragover=\"dropZone.onDragOver\"\n @dragleave=\"dropZone.onDragLeave\"\n @drop=\"dropZone.onDropFiles\"\n >\n <!-- В custom-UI контрол рисует слот (он и вызывает `openDialog`), поэтому там\n input прячется и от таба, и от дерева доступности — иначе получим два\n контрола на один смысл. -->\n <input\n :id=\"fieldId\"\n ref=\"inputRef\"\n data-gr-file-upload-input\n :style=\"hiddenInputStyle\"\n type=\"file\"\n :tabindex=\"hasCustomUi || isDisabled ? -1 : 0\"\n :aria-hidden=\"hasCustomUi ? 'true' : undefined\"\n :aria-label=\"ariaLabel ?? (hasCustomUi ? undefined : resolvedPlaceholder)\"\n :aria-describedby=\"describedBy\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :name=\"name\"\n :accept=\"accept\"\n :capture=\"capture\"\n :webkitdirectory=\"directory || undefined\"\n :multiple=\"multiple\"\n :disabled=\"isDisabled\"\n @click=\"onInputClick\"\n @change=\"onInputChange\"\n >\n\n <span\n data-gr-file-upload-live\n class=\"sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n >{{ liveMessage }}</span>\n\n <slot v-if=\"hasCustomUi\" v-bind=\"defaultSlotProps\" />\n\n <div v-else class=\"flex items-start\" :class=\"zoneGapClass\">\n <div\n data-gr-file-upload-icon\n class=\"shrink-0 bg-[var(--gr-muted)] border border-[var(--gr-brd)] flex items-center justify-center\"\n :class=\"iconTileClass\"\n aria-hidden=\"true\"\n >\n <GrIcon class=\"text-[var(--gr-muted-fg)]\" :size=\"iconGlyphSize\">\n <IconArrowUp />\n </GrIcon>\n </div>\n\n <div class=\"min-w-0\">\n <div data-gr-file-upload-label class=\"font-700\" :class=\"labelClass\">\n <slot name=\"label\">\n <slot v-bind=\"defaultSlotProps\">\n {{ resolvedPlaceholder }}\n </slot>\n </slot>\n </div>\n\n <div v-if=\"$slots.tip\" data-gr-file-upload-tip class=\"mt-1 text-[var(--gr-muted-fg)]\" :class=\"hintClass\">\n <slot name=\"tip\" />\n </div>\n <div v-else class=\"mt-1 text-[var(--gr-muted-fg)]\" :class=\"hintClass\" />\n\n <ul v-if=\"showFileList && lastFiles.length\" data-gr-file-upload-list class=\"mt-3 space-y-1\">\n <li\n v-for=\"file in lastFiles\"\n :key=\"fileKey(file)\"\n data-gr-file-upload-item\n class=\"flex items-center gap-2\"\n :class=\"hintClass\"\n >\n <img\n v-if=\"previewUrl(file)\"\n data-gr-file-upload-preview\n :src=\"previewUrl(file)\"\n alt=\"\"\n class=\"h-8 w-8 shrink-0 rounded-[var(--gr-radius-sm)] object-cover border border-[var(--gr-brd)]\"\n >\n <span class=\"font-600\">{{ file.name }}</span>\n <span class=\"text-[var(--gr-muted-fg)]\"> · {{ Math.ceil(file.size / 1024) }} KB</span>\n\n <template v-if=\"entryFor(file)\">\n <span\n data-gr-file-upload-status\n :data-status=\"entryFor(file)!.status\"\n class=\"text-[var(--gr-muted-fg)]\"\n >· {{ statusTextByStatus[entryFor(file)!.status] }}<template\n v-if=\"entryFor(file)!.status === 'uploading'\"\n >&nbsp;{{ Math.round(entryFor(file)!.percent) }}%</template></span>\n\n <button\n v-if=\"entryFor(file)!.status === 'uploading'\"\n data-gr-file-upload-abort-file\n type=\"button\"\n class=\"ml-auto text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] rounded-[var(--gr-radius-sm)]\"\n :aria-label=\"t('gr.fileUpload.abortFile', 'Cancel upload of {fileName}', { fileName: file.name })\"\n @click.stop=\"abortFile(file)\"\n >\n <GrIcon :size=\"iconGlyphSize\">\n <IconClose />\n </GrIcon>\n </button>\n\n <button\n v-else-if=\"entryFor(file)!.status === 'error' && !isDisabled && !isReadonly\"\n data-gr-file-upload-retry-file\n type=\"button\"\n class=\"ml-auto text-[var(--gr-danger-text)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] rounded-[var(--gr-radius-sm)] underline\"\n :aria-label=\"t('gr.fileUpload.retryFile', 'Retry {fileName}', { fileName: file.name })\"\n @click.stop=\"retryFile(file)\"\n >\n {{ t('gr.fileUpload.retry', 'Retry upload') }}\n </button>\n </template>\n\n <button\n v-if=\"!isDisabled && !isReadonly\"\n data-gr-file-upload-remove\n type=\"button\"\n class=\"ml-auto text-[var(--gr-muted-fg)] hover:text-[var(--gr-danger-text)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] rounded-[var(--gr-radius-sm)]\"\n :aria-label=\"t('gr.fileUpload.remove', 'Remove {fileName}', { fileName: file.name })\"\n @click.stop=\"removeFile(file)\"\n >\n <GrIcon :size=\"iconGlyphSize\">\n <IconClose />\n </GrIcon>\n </button>\n </li>\n </ul>\n\n <slot\n v-if=\"showProgress || $slots.progress\"\n name=\"progress\"\n :state=\"state\"\n :percent=\"state.percent\"\n :indeterminate=\"state.phase === 'uploading' && state.indeterminate\"\n :phase=\"state.phase\"\n :files=\"lastFiles\"\n :abort=\"abort\"\n :retry=\"retry\"\n :file-entries=\"fileEntries\"\n :retry-file=\"retryFile\"\n :abort-file=\"abortFile\"\n >\n <div\n v-if=\"showProgress\"\n data-gr-file-upload-progress\n class=\"mt-3 transition-opacity duration-[var(--gr-duration-fast)]\"\n :class=\"progressVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'\"\n :aria-hidden=\"!progressVisible\"\n >\n <div class=\"flex items-center gap-2\">\n <GrProgressBar\n :value=\"progressPercent\"\n :indeterminate=\"progressIndeterminate\"\n :tone=\"effectiveProgressTone\"\n :size=\"progressBarSize\"\n :aria-label=\"resolvedProgressLabel\"\n />\n <span\n data-gr-file-upload-progress-text\n class=\"text-[var(--gr-muted-fg)] tabular-nums min-w-[3ch] text-right\"\n :class=\"progressTextClass\"\n >{{ progressText }}</span>\n </div>\n </div>\n </slot>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\" generic=\"TResponse = unknown\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, Text, useSlots, watch, type VNode } from 'vue'\n\nimport IconArrowUp from '~icons/lucide/arrow-up'\nimport IconClose from '~icons/lucide/x'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport GrProgressBar from '../GrProgressBar/GrProgressBar.vue'\nimport type { GrProgressBarTone } from '../GrProgressBar/grStyle'\nimport type { FileValidator, FileValidatorSource } from '../../fileValidation'\nimport type { GrUploadProgressInfo } from './uploadViaXhr'\nimport type { GrUploadState } from './uploadState'\nimport { summarizeFileEntries, type GrFileUploadEntry } from './fileEntry'\nimport { useDropZone } from './useDropZone'\nimport { useFilePreviews } from '../../composables/internal/useFilePreviews'\nimport { usePerFileUpload } from './usePerFileUpload'\nimport { useUploadState } from './useUploadState'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { flattenSlotNodes, meaningfulSlotNodes } from '../shared/slotNodes'\nimport {\n type GrFileUploadSize,\n grFileUploadZoneClass,\n hintSizes,\n iconGlyphSizes,\n iconTileSizes,\n labelSizes,\n progressBarSizes,\n progressTextSizes,\n zoneGaps,\n} from './grFileUploadStyles'\nimport { acceptValidator, FileValidationError, runFileValidators } from '../../fileValidation'\nimport { GrUploadAbortError, uploadViaXhr } from './uploadViaXhr'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nexport type GrFileUploadMode = 'batch' | 'per-file'\n\nexport type GrFileUploadExtraDataValue = string | Blob\n\nexport type GrFileUploadExtraData = Record<string, GrFileUploadExtraDataValue | GrFileUploadExtraDataValue[]>\n\nexport type GrFileUploadRequestCtx = {\n signal: AbortSignal\n extraData?: GrFileUploadExtraData\n /**\n * Колбэк прогресса аплоада. Пользовательский `request` (например, axios)\n * должен вызывать его из `onUploadProgress`, чтобы `GrFileUpload` отображал\n * прогресс через слот `progress` / дефолтный `GrProgressBar`.\n */\n onProgress?: (info: GrUploadProgressInfo) => void\n}\n\nexport type GrFileUploadRequest<TResponse = unknown> = (\n files: File[],\n ctx: GrFileUploadRequestCtx,\n) => Promise<TResponse>\n\n/**\n * Пропы `GrFileUpload`.\n *\n * Либо `action` (URL для POST multipart/form-data), либо `request` — кастомный\n * загрузчик (например, через axios). Если переданы оба — приоритет у `request`.\n *\n * `placeholder` — надпись в дефолтном UI-варианте (без слота default).\n */\nexport interface GrFileUploadProps<TResponse = unknown> {\n /**\n * Набор файлов — тот, что показан в списке и уйдёт в `retry`.\n *\n * Проп **необязателен**: без него компонент держит набор сам. Передан —\n * набор следует за ним, и потребитель может очистить или подменить список\n * снаружи.\n */\n modelValue?: File[]\n action?: string\n request?: GrFileUploadRequest<TResponse>\n name?: string\n multiple?: boolean\n limit?: number\n /**\n * Guard перед отправкой. Остаётся пропом, а не эмитом, осознанно: эмит не\n * возвращает значения, а этот колбэк обязан ответить «пускать или нет».\n * Уведомления — `exceed`, `success`, `error`, `progress` — эмиты.\n */\n beforeUpload?: (file: File) => boolean | Promise<unknown>\n validators?: FileValidator[]\n /**\n * W3C `accept` для `<input type=\"file\">` — и sugar к `acceptValidator(...)`.\n *\n * Только атрибута мало: он фильтрует системный диалог, но не drag&drop —\n * перетащить можно что угодно. Поэтому то же значение уходит и в валидаторы,\n * как в `GrFormFile`.\n */\n accept?: string\n /** `capture` для мобильной камеры/микрофона. */\n capture?: 'user' | 'environment'\n /** Выбор каталога целиком (`webkitdirectory`). Поддержка — Chromium и Safari. */\n directory?: boolean\n disabled?: boolean\n /** Только для чтения: значение видно и уходит в форму, но не редактируется. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n headers?: Record<string, string>\n withCredentials?: boolean\n showFileList?: boolean\n uploadExtraData?: (files: File[]) => GrFileUploadExtraData | undefined\n /** i18n: надпись-подсказка в дефолтном UI. */\n placeholder?: string\n /** Показывать дефолтный прогресс-бар (если не используется слот `progress`). */\n showProgress?: boolean\n /** Цветовой тон полосы прогресса в фазе `uploading`. */\n progressTone?: GrProgressBarTone\n /** aria-label для прогресс-бара. */\n progressLabel?: string\n /** Через сколько мс после `success` скрыть прогресс-бар. `0` — не скрывать. */\n hideProgressOnSuccess?: number\n /** Размер дефолтного UI: поля дроп-зоны, плитка иконки, кегль подписей. */\n size?: GrFileUploadSize\n /**\n * Как уходит набор файлов:\n *\n * - `batch` (по умолчанию) — весь набор одним запросом. Про отдельный файл\n * сказать нечего, поэтому и статуса у него нет;\n * - `per-file` — на каждый файл свой запрос (`request` зовётся с массивом из\n * одного файла, контракт не меняется). Появляются статус и процент строки,\n * `retryFile` и `abortFile`.\n */\n uploadMode?: GrFileUploadMode\n /** Сколько файлов грузится одновременно в режиме `per-file`. */\n concurrency?: number\n /** Миниатюры для `image/*` в списке файлов. */\n preview?: boolean\n}\n\nexport interface GrFileUploadEmits<TResponse = unknown> {\n /** Набор файлов сменился: новый выбор или удаление. Не путать с `change`. */\n (e: 'update:modelValue', files: File[]): void\n /** Выбрано больше файлов, чем разрешает `limit`. Загрузка не стартует. */\n (e: 'exceed', files: File[], limit: number): void\n /** `file` приходит только в режиме `per-file`: в батче отчитываться нечем. */\n (e: 'success', payload: TResponse, file?: File): void\n (e: 'error', error: unknown, file?: File): void\n (e: 'progress', percent: number, info?: GrUploadProgressInfo, file?: File): void\n (e: 'change', files: File[]): void\n (e: 'stateChange', state: GrUploadState): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrFileUploadProps<TResponse>>(),\n {\n modelValue: undefined,\n action: undefined,\n request: undefined,\n name: 'file',\n multiple: false,\n limit: undefined,\n beforeUpload: undefined,\n validators: undefined,\n accept: undefined,\n capture: undefined,\n directory: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n headers: undefined,\n withCredentials: false,\n showFileList: false,\n uploadExtraData: undefined,\n placeholder: undefined,\n showProgress: true,\n progressTone: 'primary',\n progressLabel: undefined,\n hideProgressOnSuccess: 800,\n size: undefined,\n uploadMode: 'batch',\n concurrency: 3,\n preview: false,\n },\n)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFileUpload' })\n\nconst zoneGapClass = computed(() => zoneGaps[resolvedSize.value])\nconst iconTileClass = computed(() => iconTileSizes[resolvedSize.value])\nconst iconGlyphSize = computed(() => iconGlyphSizes[resolvedSize.value])\nconst labelClass = computed(() => labelSizes[resolvedSize.value])\nconst hintClass = computed(() => hintSizes[resolvedSize.value])\nconst progressTextClass = computed(() => progressTextSizes[resolvedSize.value])\nconst progressBarSize = computed(() => progressBarSizes[resolvedSize.value])\n\nconst emit = defineEmits<GrFileUploadEmits<TResponse>>()\ndefineSlots<{\n /** Полностью своя зона загрузки: контрол отдаёт наружу всё своё состояние. */\n default?: (props: {\n openDialog: () => void\n abort: () => void\n disabled: boolean\n files: File[]\n isOver: boolean\n state: GrUploadState\n retry: () => Promise<void>\n removeFile: (file: File) => void\n fileEntries: GrFileUploadEntry[]\n retryFile: (file: File) => Promise<void>\n abortFile: (file: File) => void\n }) => any\n /** Заголовок зоны вместо `placeholder`. */\n label?: () => any\n /** Подпись под заголовком: ограничения по типу и размеру. */\n tip?: () => any\n /** Свой индикатор прогресса вместо встроенного. */\n progress?: (props: {\n state: GrUploadState\n percent: number\n indeterminate: boolean\n phase: GrUploadState['phase']\n files: File[]\n abort: () => void\n retry: () => Promise<void>\n fileEntries: GrFileUploadEntry[]\n retryFile: (file: File) => Promise<void>\n abortFile: (file: File) => void\n }) => any\n}>()\n\nconst slots = useSlots()\n\nconst { t } = useGranularityTranslations()\n\n// Контекст `GrFormField`: доступным контролом служит сам нативный\n// `<input type=\"file\">` — он и остаётся целью `<label for>`.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n readonly: isReadonly,\n invalid: isInvalid,\n required: isRequired,\n // `locked` — «ввод не принимается»: `disabled` или `readonly`. Набор в\n // `readonly` виден и уходит в форму, но поменять его нельзя.\n locked: isLocked,\n} = useGrFormControl(() => props)\nconst resolvedPlaceholder = computed(() => props.placeholder ?? t('gr.fileUpload.placeholder', 'Drag files here or click to select'))\nconst resolvedProgressLabel = computed(() => props.progressLabel ?? t('gr.fileUpload.progress', 'Upload progress'))\n\nfunction slotIsTextOnly(nodes: VNode[]): boolean {\n const meaningful = meaningfulSlotNodes(nodes)\n\n if (meaningful.length === 0)\n return true\n\n for (const node of meaningful) {\n if (node.type !== Text)\n return false\n }\n\n return true\n}\n\nconst defaultSlotNodes = computed(() => {\n return slots.default ? flattenSlotNodes(slots.default({} as any)) : []\n})\n\nconst hasCustomUi = computed(() => {\n return !!slots.default && !slotIsTextOnly(defaultSlotNodes.value)\n})\n\nconst inputRef = ref<HTMLInputElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Внутри зоны фокус ходит между нативным input'ом и кнопками списка файлов:\n// без границы каждое перемещение давало бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n\nconst lastFiles = ref<File[]>(props.modelValue ? [...props.modelValue] : [])\n\n/**\n * Набор меняется только здесь: иначе `update:modelValue` рано или поздно\n * разошёлся бы с тем, что показано в списке.\n */\nfunction setFiles(next: File[]): void {\n lastFiles.value = next\n emit('update:modelValue', next)\n}\n\n// Контролируемый режим: проп задан — он и есть источник истины.\nwatch(() => props.modelValue, (next) => {\n if (next === undefined)\n return\n if (next === lastFiles.value)\n return\n\n lastFiles.value = [...next]\n}, { deep: false })\n\nconst {\n state,\n setIdle: setStateIdle,\n setUploading: setStateUploading,\n setSuccess: setStateSuccess,\n setError: setStateError,\n applyProgress,\n assign: assignState,\n scheduleIdle,\n clearScheduledIdle,\n} = useUploadState({ onChange: next => emit('stateChange', next) })\n\nlet activeController: AbortController | null = null\n\nfunction handleProgress(info: GrUploadProgressInfo) {\n applyProgress(info)\n emit('progress', info.percent, info)\n}\n\nconst { fileKey, previewUrl, revokePreview, revokeAllPreviews } = useFilePreviews({\n enabled: () => props.preview,\n})\n\n/**\n * `extraData` считается один раз на набор и не меняется между файлами, поэтому\n * живёт рядом с очередью, а не путешествует через её контракт.\n */\nlet perFileExtraData: GrFileUploadExtraData | undefined\n\nconst perFile = usePerFileUpload({\n concurrency: () => props.concurrency,\n send: (files, ctx) => (props.request\n ? props.request(files, { signal: ctx.signal, extraData: perFileExtraData, onProgress: ctx.onProgress })\n : uploadViaXhr({\n url: props.action ?? '',\n files,\n name: props.name,\n headers: props.headers,\n withCredentials: props.withCredentials,\n extraData: perFileExtraData,\n signal: ctx.signal,\n onProgress: ctx.onProgress,\n })),\n onSummary: assignState,\n onProgress: (info, file) => emit('progress', info.percent, info, file),\n onSuccess: (payload, file) => emit('success', payload as TResponse, file),\n onError: (error, file) => emit('error', error, file),\n onSetChange: files => emit('change', files),\n})\n\nconst fileEntries = perFile.entries\nconst { retryFile, abortFile } = perFile\n\nconst dropZone = useDropZone({\n locked: () => isLocked.value,\n inactive: () => hasCustomUi.value,\n onDrop: files => void handleFiles(files, 'drop'),\n})\nconst isOver = dropZone.isOver\n\n/**\n * Дефолтный слот вызывается из двух мест — как своя зона целиком и как подпись\n * внутри стандартной. Набор пропов у него один: две копии разъехались бы.\n */\nconst defaultSlotProps = computed(() => ({\n openDialog,\n abort,\n disabled: isDisabled.value,\n files: lastFiles.value,\n isOver: isOver.value,\n state,\n retry,\n removeFile,\n fileEntries: fileEntries.value,\n retryFile,\n abortFile,\n}))\n\nconst zoneClass = computed(() => grFileUploadZoneClass({\n size: resolvedSize.value,\n disabled: isDisabled.value,\n readonly: isReadonly.value,\n over: isOver.value,\n}))\n\nconst hiddenInputStyle = {\n position: 'fixed',\n top: '-9999px',\n left: '-9999px',\n width: '1px',\n height: '1px',\n opacity: '0',\n overflow: 'hidden',\n pointerEvents: 'none',\n} as const satisfies Record<string, string>\n\nfunction openDialog() {\n if (isLocked.value)\n return\n inputRef.value?.click()\n}\n\nfunction abort() {\n activeController?.abort()\n activeController = null\n}\n\nfunction totalSizeOf(files: File[]): number {\n return files.reduce((sum, file) => sum + file.size, 0)\n}\n\nfunction normalizeLimit(limit: number | undefined): number | undefined {\n if (typeof limit !== 'number')\n return undefined\n if (!Number.isFinite(limit))\n return undefined\n if (limit <= 0)\n return undefined\n return Math.floor(limit)\n}\n\nasync function uploadViaAction(files: File[], signal: AbortSignal, extraData: GrFileUploadExtraData | undefined) {\n if (!props.action)\n throw new Error('GrFileUpload: either `action` or `request` must be provided')\n\n return uploadViaXhr({\n url: props.action,\n files,\n name: props.name,\n headers: props.headers,\n withCredentials: props.withCredentials,\n extraData,\n signal,\n onProgress: handleProgress,\n })\n}\n\nasync function runBeforeUpload(files: File[]): Promise<'ok' | { aborted: true, reason: unknown }> {\n if (!props.beforeUpload)\n return 'ok'\n\n for (const file of files) {\n try {\n const result = props.beforeUpload(file)\n\n if (result === false) {\n return { aborted: true, reason: new Error('Upload aborted by beforeUpload') }\n }\n\n if (result && typeof (result as any).then === 'function')\n await result\n }\n catch (error) {\n return { aborted: true, reason: error }\n }\n }\n\n return 'ok'\n}\n\n/**\n * `accept` идёт первым валидатором: атрибут на input фильтрует только диалог,\n * а drop мимо него проходит.\n */\nfunction effectiveValidators(): FileValidator[] {\n return [acceptValidator(props.accept), ...(props.validators ?? [])]\n}\n\n/**\n * Номер запуска. Валидация и `beforeUpload` асинхронны, поэтому два быстрых\n * выбора подряд идут внахлёст: без номера «побеждал» тот, чьи валидаторы\n * отработали позже, — он же обрывал уже стартовавшую загрузку соседа.\n * Актуален всегда последний выбор, остальные тихо сходят с дистанции.\n */\nlet runCounter = 0\n\nasync function handleFiles(files: File[], source: FileValidatorSource = 'input') {\n if (isLocked.value)\n return\n if (!files.length)\n return\n\n runCounter += 1\n const runId = runCounter\n const isStale = (): boolean => runId !== runCounter\n\n const normalizedLimit = normalizeLimit(props.limit)\n if (props.multiple && normalizedLimit && files.length > normalizedLimit) {\n emit('exceed', files, normalizedLimit)\n emit('error', new Error(`Too many files selected, limit=${normalizedLimit}`))\n return\n }\n\n const { files: valid, issues } = await runFileValidators(files, effectiveValidators(), {\n source,\n multiple: props.multiple,\n })\n\n if (isStale())\n return\n\n if (issues.length > 0) {\n emit('error', new FileValidationError(issues, valid))\n return\n }\n\n if (!valid.length)\n return\n\n // Набор сменился — миниатюры прежнего больше не нужны.\n revokeAllPreviews()\n setFiles(valid)\n\n const before = await runBeforeUpload(valid)\n if (isStale())\n return\n\n if (before !== 'ok') {\n emit('error', before.reason)\n return\n }\n\n let extraData: GrFileUploadExtraData | undefined\n try {\n extraData = props.uploadExtraData?.(valid)\n }\n catch (error) {\n emit('error', error)\n return\n }\n\n abort()\n clearScheduledIdle()\n\n if (props.uploadMode === 'per-file') {\n perFileExtraData = extraData\n await perFile.run(valid)\n return\n }\n\n const controller = new AbortController()\n activeController = controller\n\n // Батч: пофайловых записей нет, показываем набор без статусов.\n perFile.reset()\n\n setStateUploading()\n emit('progress', 0, { percent: 0, loaded: 0, total: 0, indeterminate: true })\n\n try {\n // `action`-ветка отдаёт разобранный ответ сервера: типом его знает только\n // потребитель, поэтому приводим к его же `TResponse`.\n const payload = (props.request\n ? await props.request(valid, { signal: controller.signal, extraData, onProgress: handleProgress })\n : await uploadViaAction(valid, controller.signal, extraData)) as TResponse\n\n // Кастомный `request` не обязан звать `onProgress` — тогда байты неизвестны.\n // Сумма размеров выбранных файлов честнее нуля: «100%» при `total: 0`\n // потребитель прочитает как «загружено ноль».\n const measured = state.phase === 'uploading' ? state.total || state.loaded : 0\n const finalLoaded = measured || totalSizeOf(valid)\n setStateSuccess({ loaded: finalLoaded, total: finalLoaded })\n emit('success', payload)\n emit('change', valid)\n emit('progress', 100, { percent: 100, loaded: finalLoaded, total: finalLoaded, indeterminate: false })\n\n if (props.hideProgressOnSuccess && props.hideProgressOnSuccess > 0)\n scheduleIdle(props.hideProgressOnSuccess)\n }\n catch (error) {\n if (error instanceof GrUploadAbortError) {\n setStateIdle()\n }\n else {\n setStateError(error)\n }\n emit('error', error)\n }\n finally {\n if (activeController === controller)\n activeController = null\n }\n}\n\n/**\n * `<input type=\"file\">` не понимает `readonly` — атрибута для него в HTML нет.\n * Поэтому системный диалог гасим отменой действия по умолчанию: input остаётся\n * в порядке `Tab` и объявляется как «только чтение», но нового файла не примет.\n */\nfunction onInputClick(event: MouseEvent) {\n if (isReadonly.value && !isDisabled.value)\n event.preventDefault()\n}\n\nasync function onInputChange(event: Event) {\n if (isLocked.value)\n return\n\n const target = event.target as HTMLInputElement | null\n const files = target?.files ? Array.prototype.slice.call(target.files) as File[] : []\n\n if (target)\n target.value = ''\n\n await nextTick()\n await handleFiles(files, 'input')\n}\n\n// Клавиатуры на зоне нет: Enter/Space обрабатывает сам file-input, на котором и\n// стоит фокус. Собственный обработчик здесь только дублировал бы нативный и\n// открывал диалог дважды.\nfunction onRootClick(event: MouseEvent) {\n if (hasCustomUi.value)\n return\n // `openDialog` кликает по input программно, и этот клик всплывает обратно сюда.\n // Без отсечки зона открывала бы диалог по кругу.\n if (event.target === inputRef.value)\n return\n\n openDialog()\n}\n\nconst effectiveProgressTone = computed<GrProgressBarTone>(() => {\n if (state.phase === 'error')\n return 'danger'\n if (state.phase === 'success')\n return 'success'\n return props.progressTone\n})\n\n/**\n * Живой регион существует с первого рендера и пуст, пока объявлять нечего:\n * регион, появляющийся сразу с текстом, часть AT не объявляет вовсе. Прогресс\n * в процентах сюда не идёт — диктор захлебнётся; объявляются только фазы.\n */\nconst liveMessage = computed(() => {\n if (state.phase === 'uploading')\n return t('gr.fileUpload.uploading', 'Uploading…')\n if (state.phase === 'success')\n return t('gr.fileUpload.success', 'Upload complete')\n if (state.phase === 'error')\n return t('gr.fileUpload.error', 'Upload failed')\n return ''\n})\n\nconst progressVisible = computed(() => state.phase !== 'idle')\nconst progressIndeterminate = computed(() => state.phase === 'uploading' && state.indeterminate)\nconst progressPercent = computed(() => (progressIndeterminate.value ? 0 : state.percent))\nconst progressText = computed(() => {\n if (progressIndeterminate.value)\n return ''\n return `${Math.round(progressPercent.value)}%`\n})\n\nconst isPerFile = computed(() => props.uploadMode === 'per-file')\n\nconst statusTextByStatus = computed<Record<string, string>>(() => ({\n pending: t('gr.fileUpload.statusPending', 'Pending'),\n uploading: t('gr.fileUpload.statusUploading', 'Uploading'),\n success: t('gr.fileUpload.statusSuccess', 'Uploaded'),\n error: t('gr.fileUpload.statusError', 'Failed'),\n}))\n\n/** Запись файла для разметки: в батчевом режиме её нет — и статуса тоже. */\nfunction entryFor(file: File): GrFileUploadEntry | undefined {\n return isPerFile.value ? perFile.entryOf(file) : undefined\n}\n\n/** Повторить загрузку текущего набора — после ошибки выбирать файлы заново незачем. */\nasync function retry(): Promise<void> {\n if (!lastFiles.value.length)\n return\n\n await handleFiles([...lastFiles.value], 'input')\n}\n\n/**\n * Убрать файл из набора. Идущая загрузка при этом обрывается: она была про\n * прежний набор, и её результат к новому отношения не имеет.\n */\nfunction removeFile(file: File): void {\n const next = lastFiles.value.filter(item => item !== file)\n if (next.length === lastFiles.value.length)\n return\n\n abortFile(file)\n abort()\n clearScheduledIdle()\n revokePreview(file)\n setFiles(next)\n perFile.dropEntry(file)\n\n // В пофайловом режиме остальные файлы своих статусов не теряют: удалили один\n // — сводное состояние просто пересчитывается по оставшимся.\n if (props.uploadMode === 'per-file' && fileEntries.value.length)\n assignState(summarizeFileEntries(fileEntries.value))\n else if (state.phase !== 'idle')\n setStateIdle()\n}\n\nfunction focus(): void {\n inputRef.value?.focus()\n}\n\nfunction blur(): void {\n inputRef.value?.blur()\n}\n\n// Ни `action`, ни `request` — загружать некуда, и узнать об этом на первом же\n// выборе файла поздно. Выразить требование типом нельзя: `defineProps` в SFC\n// принимает объектный тип или интерфейс, но не discriminated union.\nonMounted(() => {\n if (!__GR_DEV__)\n return\n if (props.action || props.request)\n return\n\n console.warn('[GrFileUpload] не задан ни `action`, ни `request`: отправлять файлы некуда.')\n})\n\n// Хвосты, переживающие размонтирование: незавершённый XHR продолжает качать\n// файл, а таймер скрытия успеха дёргает `setStateIdle()` на уничтоженном\n// инстансе. Сценарий обычный — успешная загрузка и уход со страницы сразу после.\nonBeforeUnmount(() => {\n clearScheduledIdle()\n abort()\n perFile.abortAll()\n revokeAllPreviews()\n})\n\ndefineExpose({\n focus,\n blur,\n uploadFiles: handleFiles,\n abort,\n openDialog,\n state,\n /** Текущий набор файлов — тот, что показан в списке и уйдёт в `retry`. */\n files: lastFiles,\n retry,\n removeFile,\n /** Записи по файлам — пусты в батчевом режиме. */\n fileEntries,\n retryFile,\n abortFile,\n})\n\n/**\n * Drop-zone намеренно БЕЗ `role=\"button\"` и `tabindex`: доступным контролом\n * служит сам нативный `<input type=\"file\">`. Роль-виджет объявляет потомков\n * презентационными, поэтому input внутри неё теряется для скринридеров и\n * падает в axe на `nested-interactive`; а обёртывать его в собственный\n * «button» — значит вручную переизобретать то, что file-input уже умеет\n * нативно (открытие диалога с клавиатуры, объявление «кнопка выбора файла»).\n * Зона остаётся кликабельной и принимает drag&drop, фокус input'а показывает\n * через `focus-within`.\n */\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-file-upload\n :class=\"hasCustomUi ? 'inline-block' : zoneClass\"\n @click=\"onRootClick\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n @dragenter=\"dropZone.onDragEnter\"\n @dragover=\"dropZone.onDragOver\"\n @dragleave=\"dropZone.onDragLeave\"\n @drop=\"dropZone.onDropFiles\"\n >\n <!-- В custom-UI контрол рисует слот (он и вызывает `openDialog`), поэтому там\n input прячется и от таба, и от дерева доступности — иначе получим два\n контрола на один смысл. -->\n <input\n :id=\"fieldId\"\n ref=\"inputRef\"\n data-gr-file-upload-input\n :style=\"hiddenInputStyle\"\n type=\"file\"\n :tabindex=\"hasCustomUi || isDisabled ? -1 : 0\"\n :aria-hidden=\"hasCustomUi ? 'true' : undefined\"\n :aria-label=\"ariaLabel ?? (hasCustomUi ? undefined : resolvedPlaceholder)\"\n :aria-describedby=\"describedBy\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :name=\"name\"\n :accept=\"accept\"\n :capture=\"capture\"\n :webkitdirectory=\"directory || undefined\"\n :multiple=\"multiple\"\n :disabled=\"isDisabled\"\n @click=\"onInputClick\"\n @change=\"onInputChange\"\n >\n\n <span\n data-gr-file-upload-live\n class=\"sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n >{{ liveMessage }}</span>\n\n <slot v-if=\"hasCustomUi\" v-bind=\"defaultSlotProps\" />\n\n <div v-else class=\"flex items-start\" :class=\"zoneGapClass\">\n <div\n data-gr-file-upload-icon\n class=\"shrink-0 bg-[var(--gr-muted)] border border-[var(--gr-brd)] flex items-center justify-center\"\n :class=\"iconTileClass\"\n aria-hidden=\"true\"\n >\n <GrIcon class=\"text-[var(--gr-muted-fg)]\" :size=\"iconGlyphSize\">\n <IconArrowUp />\n </GrIcon>\n </div>\n\n <div class=\"min-w-0\">\n <div data-gr-file-upload-label class=\"font-700\" :class=\"labelClass\">\n <slot name=\"label\">\n <slot v-bind=\"defaultSlotProps\">\n {{ resolvedPlaceholder }}\n </slot>\n </slot>\n </div>\n\n <div v-if=\"$slots.tip\" data-gr-file-upload-tip class=\"mt-1 text-[var(--gr-muted-fg)]\" :class=\"hintClass\">\n <slot name=\"tip\" />\n </div>\n <div v-else class=\"mt-1 text-[var(--gr-muted-fg)]\" :class=\"hintClass\" />\n\n <ul v-if=\"showFileList && lastFiles.length\" data-gr-file-upload-list class=\"mt-3 space-y-1\">\n <li\n v-for=\"file in lastFiles\"\n :key=\"fileKey(file)\"\n data-gr-file-upload-item\n class=\"flex items-center gap-2\"\n :class=\"hintClass\"\n >\n <img\n v-if=\"previewUrl(file)\"\n data-gr-file-upload-preview\n :src=\"previewUrl(file)\"\n alt=\"\"\n class=\"h-8 w-8 shrink-0 rounded-[var(--gr-radius-sm)] object-cover border border-[var(--gr-brd)]\"\n >\n <span class=\"font-600\">{{ file.name }}</span>\n <span class=\"text-[var(--gr-muted-fg)]\"> · {{ Math.ceil(file.size / 1024) }} KB</span>\n\n <template v-if=\"entryFor(file)\">\n <span\n data-gr-file-upload-status\n :data-status=\"entryFor(file)!.status\"\n class=\"text-[var(--gr-muted-fg)]\"\n >· {{ statusTextByStatus[entryFor(file)!.status] }}<template\n v-if=\"entryFor(file)!.status === 'uploading'\"\n >&nbsp;{{ Math.round(entryFor(file)!.percent) }}%</template></span>\n\n <button\n v-if=\"entryFor(file)!.status === 'uploading'\"\n data-gr-file-upload-abort-file\n type=\"button\"\n class=\"ml-auto text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] rounded-[var(--gr-radius-sm)]\"\n :aria-label=\"t('gr.fileUpload.abortFile', 'Cancel upload of {fileName}', { fileName: file.name })\"\n @click.stop=\"abortFile(file)\"\n >\n <GrIcon :size=\"iconGlyphSize\">\n <IconClose />\n </GrIcon>\n </button>\n\n <button\n v-else-if=\"entryFor(file)!.status === 'error' && !isDisabled && !isReadonly\"\n data-gr-file-upload-retry-file\n type=\"button\"\n class=\"ml-auto text-[var(--gr-danger-text)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] rounded-[var(--gr-radius-sm)] underline\"\n :aria-label=\"t('gr.fileUpload.retryFile', 'Retry {fileName}', { fileName: file.name })\"\n @click.stop=\"retryFile(file)\"\n >\n {{ t('gr.fileUpload.retry', 'Retry upload') }}\n </button>\n </template>\n\n <button\n v-if=\"!isDisabled && !isReadonly\"\n data-gr-file-upload-remove\n type=\"button\"\n class=\"ml-auto text-[var(--gr-muted-fg)] hover:text-[var(--gr-danger-text)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] rounded-[var(--gr-radius-sm)]\"\n :aria-label=\"t('gr.fileUpload.remove', 'Remove {fileName}', { fileName: file.name })\"\n @click.stop=\"removeFile(file)\"\n >\n <GrIcon :size=\"iconGlyphSize\">\n <IconClose />\n </GrIcon>\n </button>\n </li>\n </ul>\n\n <slot\n v-if=\"showProgress || $slots.progress\"\n name=\"progress\"\n :state=\"state\"\n :percent=\"state.percent\"\n :indeterminate=\"state.phase === 'uploading' && state.indeterminate\"\n :phase=\"state.phase\"\n :files=\"lastFiles\"\n :abort=\"abort\"\n :retry=\"retry\"\n :file-entries=\"fileEntries\"\n :retry-file=\"retryFile\"\n :abort-file=\"abortFile\"\n >\n <div\n v-if=\"showProgress\"\n data-gr-file-upload-progress\n class=\"mt-3 transition-opacity duration-[var(--gr-duration-fast)]\"\n :class=\"progressVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'\"\n :aria-hidden=\"!progressVisible\"\n >\n <div class=\"flex items-center gap-2\">\n <GrProgressBar\n :value=\"progressPercent\"\n :indeterminate=\"progressIndeterminate\"\n :tone=\"effectiveProgressTone\"\n :size=\"progressBarSize\"\n :aria-label=\"resolvedProgressLabel\"\n />\n <span\n data-gr-file-upload-progress-text\n class=\"text-[var(--gr-muted-fg)] tabular-nums min-w-[3ch] text-right\"\n :class=\"progressTextClass\"\n >{{ progressText }}</span>\n </div>\n </div>\n </slot>\n </div>\n </div>\n </div>\n</template>\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  import { p as FileValidationError } from "../../chunks/fileValidation-D5TFBhbv.js";
2
- import { n as GrUploadAbortError, r as GrUploadHttpError, t as GrFileUpload_default } from "./chunks/GrFileUpload-LL8RZvC0.js";
2
+ import { n as GrUploadAbortError, r as GrUploadHttpError, t as GrFileUpload_default } from "./chunks/GrFileUpload-C0ROpmOY.js";
3
3
  import { n as grFileUploadSafelist, t as grFileUploadConfig } from "../../chunks/config-BlJa1Mj9.js";
4
4
  export { FileValidationError, GrFileUpload_default as GrFileUpload, GrFileUpload_default as default, GrUploadAbortError, GrUploadHttpError, grFileUploadConfig, grFileUploadSafelist };
@@ -250,6 +250,7 @@ var GrInput_default = /* @__PURE__ */ defineComponent({
250
250
  const liveMessage = computed(() => limitReached.value ? t("gr.input.limitReached", "Character limit reached") : "");
251
251
  function clear() {
252
252
  emit("update:modelValue", "");
253
+ emit("change", "");
253
254
  emit("clear");
254
255
  inputEl.value?.focus();
255
256
  }