@feugene/granularity 0.32.0 → 0.34.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.
- package/dist/chunks/{config-Dv_r92GL.js → config-2vrOFn2f.js} +2 -2
- package/dist/chunks/{config-Dv_r92GL.js.map → config-2vrOFn2f.js.map} +1 -1
- package/dist/chunks/{config-g8-JGZ_Y.js → config-BscVxZR3.js} +1 -1
- package/dist/chunks/{config-g8-JGZ_Y.js.map → config-BscVxZR3.js.map} +1 -1
- package/dist/chunks/{config-vYMJ3Cr1.js → config-CYthTVK2.js} +2 -2
- package/dist/chunks/config-CYthTVK2.js.map +1 -0
- package/dist/chunks/{config-CPSxM7XE.js → config-CsCUuukB.js} +1 -0
- package/dist/chunks/config-CsCUuukB.js.map +1 -0
- package/dist/chunks/{config-BTwYMQaz2.js → config-DBXCIb6r2.js} +4 -3
- package/dist/chunks/config-DBXCIb6r2.js.map +1 -0
- package/dist/chunks/{config-uzUuosv7.js → config-DGGaKf4R.js} +6 -5
- package/dist/chunks/config-DGGaKf4R.js.map +1 -0
- package/dist/chunks/{config-DnF472Ox.js → config-DOYz_SVK.js} +2 -2
- package/dist/chunks/{config-DnF472Ox.js.map → config-DOYz_SVK.js.map} +1 -1
- package/dist/chunks/{config-T3-DMXYm.js → config-DUNH02Fg.js} +2 -2
- package/dist/chunks/config-DUNH02Fg.js.map +1 -0
- package/dist/chunks/{generated-CSyPsWtM.js → generated-Buw6W7Te.js} +7 -0
- package/dist/chunks/{generated-CSyPsWtM.js.map → generated-Buw6W7Te.js.map} +1 -1
- package/dist/chunks/{grBadgeStyles-D8Mz73B5.js → grBadgeStyles-DV0DrAew.js} +17 -1
- package/dist/chunks/grBadgeStyles-DV0DrAew.js.map +1 -0
- package/dist/chunks/{grChipStyles-3JZApw03.js → grChipStyles-DcDyxAL0.js} +1 -1
- package/dist/chunks/{grChipStyles-3JZApw03.js.map → grChipStyles-DcDyxAL0.js.map} +1 -1
- package/dist/chunks/grDropdownStyles-DLToHWMX.js +5 -0
- package/dist/chunks/grDropdownStyles-DLToHWMX.js.map +1 -0
- package/dist/chunks/grPopoverStyles-DKwigt7b.js +91 -0
- package/dist/chunks/grPopoverStyles-DKwigt7b.js.map +1 -0
- package/dist/chunks/{grSelectStyles-Cdtd10eY.js → grSelectStyles-D9EQ-pgK.js} +2 -1
- package/dist/chunks/grSelectStyles-D9EQ-pgK.js.map +1 -0
- package/dist/chunks/{granular-provider-Da2CJO6j.js → granular-provider-3LUawDPh.js} +8 -8
- package/dist/chunks/{granular-provider-Da2CJO6j.js.map → granular-provider-3LUawDPh.js.map} +1 -1
- package/dist/chunks/{overlayTransition-0Fj5tQbq.js → overlayPanelSurface-BKeMhRx4.js} +24 -1
- package/dist/chunks/overlayPanelSurface-BKeMhRx4.js.map +1 -0
- package/dist/chunks/{safelist-CtiHPgrj.js → safelist-x8USecK8.js} +3 -1
- package/dist/chunks/safelist-x8USecK8.js.map +1 -0
- package/dist/chunks/{focusables-BkzK42SQ.js → useFocusTrap-D9bIPNF6.js} +123 -1
- package/dist/chunks/useFocusTrap-D9bIPNF6.js.map +1 -0
- package/dist/chunks/{useMenuItemsFocus-B6TldmGf.js → useMenuItemsFocus-B7Bl_aFA.js} +1 -1
- package/dist/chunks/{useMenuItemsFocus-B6TldmGf.js.map → useMenuItemsFocus-B7Bl_aFA.js.map} +1 -1
- package/dist/chunks/{useModalOverlay-D__CqLGn.js → useModalOverlay-BnAGZpcI.js} +1 -1
- package/dist/chunks/{useModalOverlay-D__CqLGn.js.map → useModalOverlay-BnAGZpcI.js.map} +1 -1
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-rYEY15UI.js → GrAutocomplete-BQsSE7sj.js} +4 -4
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-rYEY15UI.js.map → GrAutocomplete-BQsSE7sj.js.map} +1 -1
- package/dist/components/GrAutocomplete/index.js +2 -2
- package/dist/components/GrBadge/chunks/{GrBadge-D2MfThPz.js → GrBadge-DpUDXKbq.js} +9 -4
- package/dist/components/GrBadge/chunks/GrBadge-DpUDXKbq.js.map +1 -0
- package/dist/components/GrBadge/index.js +3 -3
- package/dist/components/GrBadge/styles.css +1 -1
- package/dist/components/GrChip/chunks/{GrChip-C0vU69tf.js → GrChip-BTivtwff.js} +1 -1
- package/dist/components/GrChip/chunks/{GrChip-C0vU69tf.js.map → GrChip-BTivtwff.js.map} +1 -1
- package/dist/components/GrChip/index.js +2 -2
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-p2XH_8cE.js → GrColorPicker-D1Iis7jf.js} +2 -1
- package/dist/components/GrColorPicker/chunks/GrColorPicker-D1Iis7jf.js.map +1 -0
- package/dist/components/GrColorPicker/index.js +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-BXbh3q7O.js → GrCommandPalette-BZHUKlux.js} +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-BXbh3q7O.js.map → GrCommandPalette-BZHUKlux.js.map} +1 -1
- package/dist/components/GrCommandPalette/index.js +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-CUP43q__.js → GrConfirmDialog-jRDs_E6C.js} +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-CUP43q__.js.map → GrConfirmDialog-jRDs_E6C.js.map} +1 -1
- package/dist/components/GrConfirmDialog/index.js +1 -1
- package/dist/components/GrContextMenu/chunks/{GrContextMenu-ClkVjdcP.js → GrContextMenu-CWmgrzpi.js} +17 -3
- package/dist/components/GrContextMenu/chunks/GrContextMenu-CWmgrzpi.js.map +1 -0
- package/dist/components/GrContextMenu/index.js +2 -2
- package/dist/components/GrDataTable/chunks/{GrDataTable-CAGgRk27.js → GrDataTable-4JAgOQ-7.js} +1 -1
- package/dist/components/GrDataTable/chunks/{GrDataTable-CAGgRk27.js.map → GrDataTable-4JAgOQ-7.js.map} +1 -1
- package/dist/components/GrDataTable/index.js +1 -1
- package/dist/components/GrDialog/chunks/{GrDialog-BiMJ9_T4.js → GrDialog-C01Hhk6G.js} +1 -1
- package/dist/components/GrDialog/chunks/{GrDialog-BiMJ9_T4.js.map → GrDialog-C01Hhk6G.js.map} +1 -1
- package/dist/components/GrDialog/index.js +1 -1
- package/dist/components/GrDialogService/chunks/{GrDialogService-BxNK6crD.js → GrDialogService-CmsJNUmM.js} +7 -2
- package/dist/components/GrDialogService/chunks/{GrDialogService-BxNK6crD.js.map → GrDialogService-CmsJNUmM.js.map} +1 -1
- package/dist/components/GrDialogService/index.js +1 -1
- package/dist/components/GrDrawer/chunks/{GrDrawer-Ce2l3zaG.js → GrDrawer-xfoy-MCj.js} +1 -1
- package/dist/components/GrDrawer/chunks/{GrDrawer-Ce2l3zaG.js.map → GrDrawer-xfoy-MCj.js.map} +1 -1
- package/dist/components/GrDrawer/index.js +1 -1
- package/dist/components/GrDropdown/chunks/GrDropdown-CDkPkvIJ.js +180 -0
- package/dist/components/GrDropdown/chunks/GrDropdown-CDkPkvIJ.js.map +1 -0
- package/dist/components/GrDropdown/index.js +2 -2
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-GOzuAXsi.js → GrDropdownMenu-DQrZ_yPV.js} +1 -1
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-GOzuAXsi.js.map → GrDropdownMenu-DQrZ_yPV.js.map} +1 -1
- package/dist/components/GrDropdownMenu/index.js +1 -1
- package/dist/components/GrFileUpload/chunks/{GrFileUpload-NsHkRRIa.js → GrFileUpload-LL8RZvC0.js} +10 -0
- package/dist/components/GrFileUpload/chunks/GrFileUpload-LL8RZvC0.js.map +1 -0
- package/dist/components/GrFileUpload/index.js +1 -1
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-CG4vUBg_.js → GrImageViewer--BU8yrfx.js} +1 -1
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-CG4vUBg_.js.map → GrImageViewer--BU8yrfx.js.map} +1 -1
- package/dist/components/GrImageViewer/index.js +1 -1
- package/dist/components/GrInputTag/chunks/{GrInputTag-Cj4A9Xgs.js → GrInputTag-ByD9NugQ.js} +2 -2
- package/dist/components/GrInputTag/chunks/{GrInputTag-Cj4A9Xgs.js.map → GrInputTag-ByD9NugQ.js.map} +1 -1
- package/dist/components/GrInputTag/index.js +1 -1
- package/dist/components/GrModal/chunks/{GrModal-D0mf1SAx.js → GrModal-_JrDhon-.js} +1 -1
- package/dist/components/GrModal/chunks/{GrModal-D0mf1SAx.js.map → GrModal-_JrDhon-.js.map} +1 -1
- package/dist/components/GrModal/index.js +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-B_kAhoAV.js → GrPagination-gBoWVHk6.js} +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-B_kAhoAV.js.map → GrPagination-gBoWVHk6.js.map} +1 -1
- package/dist/components/GrPagination/index.js +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-faJanmGs.js → GrPopover-CzeXMcvh.js} +87 -13
- package/dist/components/GrPopover/chunks/GrPopover-CzeXMcvh.js.map +1 -0
- package/dist/components/GrPopover/index.js +2 -2
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-C6QGC7QJ.js → GrPromptDialog-CailXSqW.js} +1 -1
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-C6QGC7QJ.js.map → GrPromptDialog-CailXSqW.js.map} +1 -1
- package/dist/components/GrPromptDialog/index.js +1 -1
- package/dist/components/GrSelect/chunks/{GrSelect-gCeRFgQo.js → GrSelect-CZqV1Eg4.js} +4 -4
- package/dist/components/GrSelect/chunks/{GrSelect-gCeRFgQo.js.map → GrSelect-CZqV1Eg4.js.map} +1 -1
- package/dist/components/GrSelect/index.js +2 -2
- package/dist/components/GrTable/chunks/{GrTable-Zcdz2St_.js → GrTable-jhUTczjv.js} +12 -1
- package/dist/components/GrTable/chunks/GrTable-jhUTczjv.js.map +1 -0
- package/dist/components/GrTable/index.js +1 -1
- package/dist/components/GrTabs/chunks/{GrTabs-ugDnkk-2.js → GrTabs-UsO4j8zv.js} +5 -0
- package/dist/components/GrTabs/chunks/GrTabs-UsO4j8zv.js.map +1 -0
- package/dist/components/GrTabs/index.js +1 -1
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-BHwgGJcT.js → GrTreeSelect-DOXcpWq2.js} +2 -2
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-BHwgGJcT.js.map → GrTreeSelect-DOXcpWq2.js.map} +1 -1
- package/dist/components/GrTreeSelect/index.js +2 -2
- package/dist/composables/useFocusTrap.js +1 -123
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/index.js +34 -34
- package/dist/theme.js +1 -1
- package/dist/tokens.js +1 -1
- package/dist/types/components/GrAutocomplete/grAutocompleteStyles.d.ts +1 -1
- package/dist/types/components/GrBadge/GrBadge.vue.d.ts +11 -0
- package/dist/types/components/GrBadge/grBadgeStyles.d.ts +11 -0
- package/dist/types/components/GrDropdown/GrDropdown.vue.d.ts +5 -5
- package/dist/types/components/GrDropdown/grDropdownStyles.d.ts +8 -2
- package/dist/types/components/GrDropdownMenu/GrDropdownMenu.vue.d.ts +2 -2
- package/dist/types/components/GrPopover/GrPopover.vue.d.ts +54 -4
- package/dist/types/components/GrPopover/grPopoverStyles.d.ts +19 -11
- package/dist/types/components/GrSelect/grSelectStyles.d.ts +1 -1
- package/dist/types/components/GrTreeSelect/grTreeSelectStyles.d.ts +1 -1
- package/dist/types/components/shared/overlayOrigin.d.ts +22 -0
- package/dist/types/components/shared/overlayPanelSurface.d.ts +21 -0
- package/dist/web-types.json +44 -4
- package/package.json +3 -3
- package/dist/chunks/config-BTwYMQaz2.js.map +0 -1
- package/dist/chunks/config-CPSxM7XE.js.map +0 -1
- package/dist/chunks/config-T3-DMXYm.js.map +0 -1
- package/dist/chunks/config-uzUuosv7.js.map +0 -1
- package/dist/chunks/config-vYMJ3Cr1.js.map +0 -1
- package/dist/chunks/focusables-BkzK42SQ.js.map +0 -1
- package/dist/chunks/grBadgeStyles-D8Mz73B5.js.map +0 -1
- package/dist/chunks/grDropdownStyles-V7TRiCXn.js +0 -24
- package/dist/chunks/grDropdownStyles-V7TRiCXn.js.map +0 -1
- package/dist/chunks/grPopoverStyles-Djh5RNhn.js +0 -59
- package/dist/chunks/grPopoverStyles-Djh5RNhn.js.map +0 -1
- package/dist/chunks/grSelectStyles-Cdtd10eY.js.map +0 -1
- package/dist/chunks/overlayTransition-0Fj5tQbq.js.map +0 -1
- package/dist/chunks/safelist-CtiHPgrj.js.map +0 -1
- package/dist/components/GrBadge/chunks/GrBadge-D2MfThPz.js.map +0 -1
- package/dist/components/GrColorPicker/chunks/GrColorPicker-p2XH_8cE.js.map +0 -1
- package/dist/components/GrContextMenu/chunks/GrContextMenu-ClkVjdcP.js.map +0 -1
- package/dist/components/GrDropdown/chunks/GrDropdown-13Wt9eB_.js +0 -239
- package/dist/components/GrDropdown/chunks/GrDropdown-13Wt9eB_.js.map +0 -1
- package/dist/components/GrFileUpload/chunks/GrFileUpload-NsHkRRIa.js.map +0 -1
- package/dist/components/GrPopover/chunks/GrPopover-faJanmGs.js.map +0 -1
- package/dist/components/GrTable/chunks/GrTable-Zcdz2St_.js.map +0 -1
- package/dist/components/GrTabs/chunks/GrTabs-ugDnkk-2.js.map +0 -1
- package/dist/composables/useFocusTrap.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrFileUpload-NsHkRRIa.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</script>\n\n<template>\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 <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 > {{ 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</script>\n\n<template>\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 <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 > {{ 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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrPopover-faJanmGs.js","names":[],"sources":["../../../../src/components/GrPopover/GrPopover.vue","../../../../src/components/GrPopover/GrPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrPopover — якорный немодальный оверлей с произвольным содержимым.\n *\n * Роль «универсального поповера» до сих пор насильно исполнял `GrDropdown`, но\n * он про меню: жёстко объявляет `role=\"menu\"`, `aria-haspopup=\"menu\"` и водит\n * фокус по пунктам. Для формы, подтверждения или палитры это неверная семантика.\n *\n * Здесь наоборот: позиционирование, слой и закрытие берёт на себя примитив, а\n * клавиатурный паттерн внутри панели остаётся за потребителем — поэтому `role`\n * задаётся пропом. На этом собираются `GrMenu` / `GrContextMenu` /\n * `GrColorPicker` / Popconfirm.\n *\n * A11y:\n * - триггер получает `aria-haspopup`/`aria-expanded`/`aria-controls` через\n * `triggerProps` (биндится на реальный фокусируемый элемент в слоте);\n * - панель — `role=\"dialog\"` по умолчанию и обязательное доступное имя\n * (`ariaLabel` либо `aria-labelledby` через `labelledBy`);\n * - Esc закрывает верхний слой стека, клик вне — закрывает по точке попадания;\n * - **по умолчанию фокус-ловушки нет намеренно**: Tab обязан уводить фокус\n * наружу, иначе немодальный слой запирает пользователя на странице, которая\n * не заблокирована. Проп `modal` включает второй режим — см. ниже.\n */\nimport { computed, nextTick, ref, useId, useSlots, watch } from 'vue'\n\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { createFloatingAnchor, type GrFloatingAnchorRect, useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { vClickOutside } from '../../directives'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport {\n type GrPopoverPadding,\n type GrPopoverRole,\n type GrPopoverSize,\n grPopoverPanelClass,\n} from './grPopoverStyles'\n\nexport interface GrPopoverProps {\n /**\n * Открыт ли поповер. Без этого пропа компонент ведёт состояние сам\n * (uncontrolled), с ним — слушайте `update:open`.\n */\n open?: boolean\n placement?: UseFloatingPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n size?: GrPopoverSize\n /** Роль панели. Меняется теми, кто строит поверх примитива своё меню/список. */\n role?: GrPopoverRole\n /**\n * Модальный режим: фон уходит в `inert`, Tab ходит по кругу внутри панели,\n * скролл страницы блокируется, а слой встаёт в стек модальным — как окно.\n * Нужен поповеру с формой или подтверждением внутри: без изоляции фона\n * пользователь уводит фокус на страницу, к которой поповер и относится.\n *\n * В этом режиме фокус переносится в панель независимо от `autoFocus`: фон\n * недоступен, и слой без фокуса внутри стал бы клавиатурной ловушкой.\n */\n modal?: boolean\n /** Доступное имя панели. Обязательно для `role=\"dialog\"` без видимого заголовка. */\n ariaLabel?: string\n /** `id` видимого заголовка внутри панели — альтернатива `ariaLabel`. */\n labelledBy?: string\n /**\n * `manual` — открывать только программно (через `v-model:open`). Нужно для\n * контекстного меню и подтверждений, где момент открытия решает потребитель.\n */\n trigger?: 'click' | 'manual'\n closeOnEsc?: boolean\n closeOnClickOutside?: boolean\n /** Закрывать по клику внутри панели — удобно для меню, вредно для формы. */\n closeOnContentClick?: boolean\n /**\n * Переносить фокус на панель при открытии. Именно на панель, а не на первый\n * контрол внутри: сфокусировать поле ввода за пользователя — решение\n * содержимого, а не оболочки.\n */\n autoFocus?: boolean\n /**\n * Точечное переопределение точки монтирования. По умолчанию — общий портал\n * оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).\n */\n teleportTo?: string | HTMLElement\n contentClass?: string\n disabled?: boolean\n /**\n * Якорь-прямоугольник в координатах вьюпорта вместо обёртки слота `#trigger`:\n * панель встаёт у точки курсора или у строки списка. Нужен контекстному меню,\n * у которого триггера-элемента нет вовсе.\n *\n * `triggerProps` в этом режиме некому потребить, и своих ARIA-атрибутов\n * поповер никуда не вешает: `aria-haspopup` невалиден вне интерактивного\n * элемента, а `aria-expanded` без хозяина — шум. Связь с содержимым страницы\n * объявляет тот, кто открывает.\n */\n anchor?: GrFloatingAnchorRect | null\n /** Поле панели. `none` — содержимое рисует своё (меню, список опций). */\n padding?: GrPopoverPadding\n}\n\nexport interface GrPopoverEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrPopoverProps>(), {\n open: undefined,\n placement: 'bottom-start',\n offsetPx: 8,\n size: undefined,\n role: 'dialog',\n modal: false,\n ariaLabel: undefined,\n labelledBy: undefined,\n trigger: 'click',\n closeOnEsc: true,\n closeOnClickOutside: true,\n closeOnContentClick: false,\n autoFocus: true,\n teleportTo: undefined,\n contentClass: undefined,\n disabled: false,\n anchor: undefined,\n padding: 'default',\n})\n\nconst emit = defineEmits<GrPopoverEmits>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPopover' })\n\nconst { open: isOpen, setOpen } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\nconst panelId = useId()\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nfunction open(): void {\n if (props.disabled)\n return\n setOpen(true)\n}\n\nfunction close(): void {\n setOpen(false)\n}\n\nfunction toggle(): void {\n isOpen.value ? close() : open()\n}\n\n/**\n * Якорь создаётся один раз и читает координаты геттером: `useFloating`\n * пересоздаёт подписку при смене самой ссылки, а точка меняется на каждый\n * вызов — новый объект рвал бы `autoUpdate` без нужды.\n */\nconst anchorEl = createFloatingAnchor(() => props.anchor)\n\n/** Обёртка триггера существует только вместе со слотом — см. проп `anchor`. */\nconst hasTrigger = computed(() => Boolean(slots.trigger))\n\nconst { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(\n () => (props.anchor ? anchorEl : rootEl.value),\n panelEl,\n isOpen,\n {\n placement: () => props.placement,\n offsetPx: props.offsetPx,\n // Своего слоя у поповера нет намеренно: он и дропдаун — один класс якорных\n // немодальных оверлеев и обязаны лежать на одной высоте, иначе начнут\n // перекрывать друг друга в зависимости от порядка открытия.\n zIndexVar: '--gr-z-dropdown',\n },\n)\n\nwatch(() => props.placement, () => {\n if (isOpen.value)\n updateFloatingPosition()\n})\n\n// Объект якоря стабилен, поэтому смену координат `useFloating` сам не заметит:\n// повторный вызов у другой точки — это пересчёт позиции, а не переоткрытие.\nwatch(() => props.anchor, () => {\n if (isOpen.value)\n updateFloatingPosition()\n}, { deep: true })\n\n/**\n * Слой стека оверлеев: очередь Esc общая с модалками, поэтому поповер, открытый\n * внутри модалки, по Esc закрывает себя, а не её. Возврат фокуса на триггер\n * тоже отсюда — по правилу «только если на момент закрытия фокус ещё внутри».\n *\n * Модальность включается пропом и приходит той же сборкой, что у окна и drawer'а\n * (`useModalOverlay`): ловушка фокуса, `inert` фона, блокировка скролла. При\n * `modal: false` не включается ничего из этого — дефолтный поповер немодален.\n *\n * Корень слоя и панель здесь один элемент: якорный оверлей уезжает в портал сам,\n * без обёртки.\n */\nconst { inertAttr, portalTarget, teleportEnabled, themeAttrs } = useModalOverlay(isOpen, close, {\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc,\n panel: panelEl,\n root: panelEl,\n teleportTo: () => props.teleportTo,\n initialFocus: () => panelEl.value,\n})\n\n// Фокус переносим после отрисовки панели: до `nextTick` её ещё нет в DOM.\n// В модальном режиме перенос обязателен: фон в `inert`, и фокус, оставленный\n// снаружи, попал бы в недоступное поддерево.\nwatch(isOpen, async (next) => {\n if (!next || !(props.autoFocus || props.modal))\n return\n\n await nextTick()\n panelEl.value?.focus()\n})\n\n/** Роли, которые `aria-haspopup` действительно понимает. */\nconst HASPOPUP_ROLES = ['dialog', 'menu', 'listbox', 'grid'] as const\n\n/**\n * Пропсы для реального фокусируемого триггера: `<button v-bind=\"triggerProps\">`.\n * Клик вешается на обёртку, а ARIA — сюда, потому что `aria-expanded` обязан\n * жить на самом интерактивном элементе, а не на `div` вокруг него.\n */\nconst triggerProps = computed(() => ({\n // `aria-haspopup` принимает не любую роль: `group` в его списке нет, и\n // объявленный им триггер отдавал бы невалидный атрибут, который AT просто\n // отбрасывает. Роли вне списка сообщают о панели только через `aria-expanded`.\n 'aria-haspopup': HASPOPUP_ROLES.includes(props.role as (typeof HASPOPUP_ROLES)[number])\n ? (props.role as (typeof HASPOPUP_ROLES)[number])\n : undefined,\n 'aria-expanded': isOpen.value,\n 'aria-controls': isOpen.value ? panelId : undefined,\n 'disabled': props.disabled || undefined,\n}))\n\nfunction onTriggerClick(): void {\n if (props.trigger === 'click')\n toggle()\n}\n\nfunction onContentClick(): void {\n if (props.closeOnContentClick)\n close()\n}\n\nconst panelClasses = computed(() =>\n grPopoverPanelClass(resolvedSize.value, resolvedPlacement.value, props.contentClass, props.padding),\n)\n\ndefineExpose({ open, close, toggle })\n\ndefineSlots<{\n /**\n * Триггер панели. `triggerProps` обязаны попасть на сам интерактивный\n * элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls`\n * читаются с того узла, который получает фокус.\n */\n trigger?: (props: {\n open: boolean\n toggle: () => void\n close: () => void\n triggerProps: Record<string, unknown>\n }) => any\n /** Содержимое панели. */\n content?: (props: { close: () => void }) => any\n}>()\n</script>\n\n<template>\n <div data-gr-popover>\n <div\n v-if=\"hasTrigger\"\n ref=\"rootEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside, exclude: clickOutsideExclude }\"\n data-gr-popover-trigger\n class=\"inline-block max-w-full\"\n @click=\"onTriggerClick\"\n >\n <slot\n name=\"trigger\"\n :open=\"isOpen\"\n :toggle=\"toggle\"\n :close=\"close\"\n :trigger-props=\"triggerProps\"\n />\n </div>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n :enter-active-class=\"panelPopTransition.enter\"\n :enter-from-class=\"panelPopTransition.enterFrom\"\n :enter-to-class=\"panelPopTransition.enterTo\"\n :leave-active-class=\"panelPopTransition.leave\"\n :leave-from-class=\"panelPopTransition.leaveFrom\"\n :leave-to-class=\"panelPopTransition.leaveTo\"\n >\n <div\n v-show=\"isOpen\"\n :id=\"panelId\"\n ref=\"panelEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside && !hasTrigger }\"\n v-bind=\"themeAttrs\"\n data-gr-popover-panel\n data-gr-overlay-root\n :role=\"role === 'none' ? undefined : role\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :inert=\"inertAttr\"\n :aria-label=\"labelledBy ? undefined : ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n tabindex=\"-1\"\n :class=\"panelClasses\"\n :style=\"floatingStyle\"\n @click=\"onContentClick\"\n >\n <slot name=\"content\" :close=\"close\" />\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrPopover — якорный немодальный оверлей с произвольным содержимым.\n *\n * Роль «универсального поповера» до сих пор насильно исполнял `GrDropdown`, но\n * он про меню: жёстко объявляет `role=\"menu\"`, `aria-haspopup=\"menu\"` и водит\n * фокус по пунктам. Для формы, подтверждения или палитры это неверная семантика.\n *\n * Здесь наоборот: позиционирование, слой и закрытие берёт на себя примитив, а\n * клавиатурный паттерн внутри панели остаётся за потребителем — поэтому `role`\n * задаётся пропом. На этом собираются `GrMenu` / `GrContextMenu` /\n * `GrColorPicker` / Popconfirm.\n *\n * A11y:\n * - триггер получает `aria-haspopup`/`aria-expanded`/`aria-controls` через\n * `triggerProps` (биндится на реальный фокусируемый элемент в слоте);\n * - панель — `role=\"dialog\"` по умолчанию и обязательное доступное имя\n * (`ariaLabel` либо `aria-labelledby` через `labelledBy`);\n * - Esc закрывает верхний слой стека, клик вне — закрывает по точке попадания;\n * - **по умолчанию фокус-ловушки нет намеренно**: Tab обязан уводить фокус\n * наружу, иначе немодальный слой запирает пользователя на странице, которая\n * не заблокирована. Проп `modal` включает второй режим — см. ниже.\n */\nimport { computed, nextTick, ref, useId, useSlots, watch } from 'vue'\n\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { createFloatingAnchor, type GrFloatingAnchorRect, useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { vClickOutside } from '../../directives'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport {\n type GrPopoverPadding,\n type GrPopoverRole,\n type GrPopoverSize,\n grPopoverPanelClass,\n} from './grPopoverStyles'\n\nexport interface GrPopoverProps {\n /**\n * Открыт ли поповер. Без этого пропа компонент ведёт состояние сам\n * (uncontrolled), с ним — слушайте `update:open`.\n */\n open?: boolean\n placement?: UseFloatingPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n size?: GrPopoverSize\n /** Роль панели. Меняется теми, кто строит поверх примитива своё меню/список. */\n role?: GrPopoverRole\n /**\n * Модальный режим: фон уходит в `inert`, Tab ходит по кругу внутри панели,\n * скролл страницы блокируется, а слой встаёт в стек модальным — как окно.\n * Нужен поповеру с формой или подтверждением внутри: без изоляции фона\n * пользователь уводит фокус на страницу, к которой поповер и относится.\n *\n * В этом режиме фокус переносится в панель независимо от `autoFocus`: фон\n * недоступен, и слой без фокуса внутри стал бы клавиатурной ловушкой.\n */\n modal?: boolean\n /** Доступное имя панели. Обязательно для `role=\"dialog\"` без видимого заголовка. */\n ariaLabel?: string\n /** `id` видимого заголовка внутри панели — альтернатива `ariaLabel`. */\n labelledBy?: string\n /**\n * `manual` — открывать только программно (через `v-model:open`). Нужно для\n * контекстного меню и подтверждений, где момент открытия решает потребитель.\n */\n trigger?: 'click' | 'manual'\n closeOnEsc?: boolean\n closeOnClickOutside?: boolean\n /** Закрывать по клику внутри панели — удобно для меню, вредно для формы. */\n closeOnContentClick?: boolean\n /**\n * Переносить фокус на панель при открытии. Именно на панель, а не на первый\n * контрол внутри: сфокусировать поле ввода за пользователя — решение\n * содержимого, а не оболочки.\n */\n autoFocus?: boolean\n /**\n * Точечное переопределение точки монтирования. По умолчанию — общий портал\n * оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).\n */\n teleportTo?: string | HTMLElement\n contentClass?: string\n disabled?: boolean\n /**\n * Якорь-прямоугольник в координатах вьюпорта вместо обёртки слота `#trigger`:\n * панель встаёт у точки курсора или у строки списка. Нужен контекстному меню,\n * у которого триггера-элемента нет вовсе.\n *\n * `triggerProps` в этом режиме некому потребить, и своих ARIA-атрибутов\n * поповер никуда не вешает: `aria-haspopup` невалиден вне интерактивного\n * элемента, а `aria-expanded` без хозяина — шум. Связь с содержимым страницы\n * объявляет тот, кто открывает.\n */\n anchor?: GrFloatingAnchorRect | null\n /** Поле панели. `none` — содержимое рисует своё (меню, список опций). */\n padding?: GrPopoverPadding\n}\n\nexport interface GrPopoverEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrPopoverProps>(), {\n open: undefined,\n placement: 'bottom-start',\n offsetPx: 8,\n size: undefined,\n role: 'dialog',\n modal: false,\n ariaLabel: undefined,\n labelledBy: undefined,\n trigger: 'click',\n closeOnEsc: true,\n closeOnClickOutside: true,\n closeOnContentClick: false,\n autoFocus: true,\n teleportTo: undefined,\n contentClass: undefined,\n disabled: false,\n anchor: undefined,\n padding: 'default',\n})\n\nconst emit = defineEmits<GrPopoverEmits>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPopover' })\n\nconst { open: isOpen, setOpen } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\nconst panelId = useId()\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nfunction open(): void {\n if (props.disabled)\n return\n setOpen(true)\n}\n\nfunction close(): void {\n setOpen(false)\n}\n\nfunction toggle(): void {\n isOpen.value ? close() : open()\n}\n\n/**\n * Якорь создаётся один раз и читает координаты геттером: `useFloating`\n * пересоздаёт подписку при смене самой ссылки, а точка меняется на каждый\n * вызов — новый объект рвал бы `autoUpdate` без нужды.\n */\nconst anchorEl = createFloatingAnchor(() => props.anchor)\n\n/** Обёртка триггера существует только вместе со слотом — см. проп `anchor`. */\nconst hasTrigger = computed(() => Boolean(slots.trigger))\n\nconst { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(\n () => (props.anchor ? anchorEl : rootEl.value),\n panelEl,\n isOpen,\n {\n placement: () => props.placement,\n offsetPx: props.offsetPx,\n // Своего слоя у поповера нет намеренно: он и дропдаун — один класс якорных\n // немодальных оверлеев и обязаны лежать на одной высоте, иначе начнут\n // перекрывать друг друга в зависимости от порядка открытия.\n zIndexVar: '--gr-z-dropdown',\n },\n)\n\nwatch(() => props.placement, () => {\n if (isOpen.value)\n updateFloatingPosition()\n})\n\n// Объект якоря стабилен, поэтому смену координат `useFloating` сам не заметит:\n// повторный вызов у другой точки — это пересчёт позиции, а не переоткрытие.\nwatch(() => props.anchor, () => {\n if (isOpen.value)\n updateFloatingPosition()\n}, { deep: true })\n\n/**\n * Слой стека оверлеев: очередь Esc общая с модалками, поэтому поповер, открытый\n * внутри модалки, по Esc закрывает себя, а не её. Возврат фокуса на триггер\n * тоже отсюда — по правилу «только если на момент закрытия фокус ещё внутри».\n *\n * Модальность включается пропом и приходит той же сборкой, что у окна и drawer'а\n * (`useModalOverlay`): ловушка фокуса, `inert` фона, блокировка скролла. При\n * `modal: false` не включается ничего из этого — дефолтный поповер немодален.\n *\n * Корень слоя и панель здесь один элемент: якорный оверлей уезжает в портал сам,\n * без обёртки.\n */\nconst { inertAttr, portalTarget, teleportEnabled, themeAttrs } = useModalOverlay(isOpen, close, {\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc,\n panel: panelEl,\n root: panelEl,\n teleportTo: () => props.teleportTo,\n initialFocus: () => panelEl.value,\n})\n\n// Фокус переносим после отрисовки панели: до `nextTick` её ещё нет в DOM.\n// В модальном режиме перенос обязателен: фон в `inert`, и фокус, оставленный\n// снаружи, попал бы в недоступное поддерево.\nwatch(isOpen, async (next) => {\n if (!next || !(props.autoFocus || props.modal))\n return\n\n await nextTick()\n panelEl.value?.focus()\n})\n\n/** Роли, которые `aria-haspopup` действительно понимает. */\nconst HASPOPUP_ROLES = ['dialog', 'menu', 'listbox', 'grid'] as const\n\n/**\n * Пропсы для реального фокусируемого триггера: `<button v-bind=\"triggerProps\">`.\n * Клик вешается на обёртку, а ARIA — сюда, потому что `aria-expanded` обязан\n * жить на самом интерактивном элементе, а не на `div` вокруг него.\n */\nconst triggerProps = computed(() => ({\n // `aria-haspopup` принимает не любую роль: `group` в его списке нет, и\n // объявленный им триггер отдавал бы невалидный атрибут, который AT просто\n // отбрасывает. Роли вне списка сообщают о панели только через `aria-expanded`.\n 'aria-haspopup': HASPOPUP_ROLES.includes(props.role as (typeof HASPOPUP_ROLES)[number])\n ? (props.role as (typeof HASPOPUP_ROLES)[number])\n : undefined,\n 'aria-expanded': isOpen.value,\n 'aria-controls': isOpen.value ? panelId : undefined,\n 'disabled': props.disabled || undefined,\n}))\n\nfunction onTriggerClick(): void {\n if (props.trigger === 'click')\n toggle()\n}\n\nfunction onContentClick(): void {\n if (props.closeOnContentClick)\n close()\n}\n\nconst panelClasses = computed(() =>\n grPopoverPanelClass(resolvedSize.value, resolvedPlacement.value, props.contentClass, props.padding),\n)\n\ndefineExpose({ open, close, toggle })\n\ndefineSlots<{\n /**\n * Триггер панели. `triggerProps` обязаны попасть на сам интерактивный\n * элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls`\n * читаются с того узла, который получает фокус.\n */\n trigger?: (props: {\n open: boolean\n toggle: () => void\n close: () => void\n triggerProps: Record<string, unknown>\n }) => any\n /** Содержимое панели. */\n content?: (props: { close: () => void }) => any\n}>()\n</script>\n\n<template>\n <div data-gr-popover>\n <div\n v-if=\"hasTrigger\"\n ref=\"rootEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside, exclude: clickOutsideExclude }\"\n data-gr-popover-trigger\n class=\"inline-block max-w-full\"\n @click=\"onTriggerClick\"\n >\n <slot\n name=\"trigger\"\n :open=\"isOpen\"\n :toggle=\"toggle\"\n :close=\"close\"\n :trigger-props=\"triggerProps\"\n />\n </div>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n :enter-active-class=\"panelPopTransition.enter\"\n :enter-from-class=\"panelPopTransition.enterFrom\"\n :enter-to-class=\"panelPopTransition.enterTo\"\n :leave-active-class=\"panelPopTransition.leave\"\n :leave-from-class=\"panelPopTransition.leaveFrom\"\n :leave-to-class=\"panelPopTransition.leaveTo\"\n >\n <div\n v-show=\"isOpen\"\n :id=\"panelId\"\n ref=\"panelEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside && !hasTrigger }\"\n v-bind=\"themeAttrs\"\n data-gr-popover-panel\n data-gr-overlay-root\n :role=\"role === 'none' ? undefined : role\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :inert=\"inertAttr\"\n :aria-label=\"labelledBy ? undefined : ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n tabindex=\"-1\"\n :class=\"panelClasses\"\n :style=\"floatingStyle\"\n @click=\"onContentClick\"\n >\n <slot name=\"content\" :close=\"close\" />\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrTable-Zcdz2St_.js","names":[],"sources":["../../../../src/components/GrTable/GrTable.vue","../../../../src/components/GrTable/GrTable.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, useSlots } from 'vue'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\nimport { hasMeaningfulSlotContent } from '../shared/slotNodes'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport {\n emptyCellClass,\n hoverableClass,\n loadingRowCellClass,\n stripedClass,\n tableSizes,\n type GrTableSize,\n} from './grTableStyles'\n\nexport interface GrTableProps {\n /**\n * Размер таблицы — базовый кегль текста. Паддинги ячеек оставлены за\n * консьюмером (GrTable — «тонкий» контейнер).\n */\n size?: GrTableSize\n /**\n * Текст caption для screen reader'ов. Рендерится как `<caption class=\"sr-only\">`,\n * если не передан слот `#caption`.\n */\n caption?: string\n /** Прямой ARIA-label для `<table>`. Игнорируется, если задан `ariaLabelledby`. */\n ariaLabel?: string\n /** ID элемента-заголовка, связанного с `<table>` через `aria-labelledby`. */\n ariaLabelledby?: string\n /**\n * ARIA-label для скролл-контейнера. Включает `role=\"region\"`; сам скролл\n * достижим с клавиатуры всегда, независимо от метки.\n */\n regionLabel?: string\n /** Идёт загрузка: вместо строк — скелетоны, контейнер помечен `aria-busy`. */\n loading?: boolean\n /** Сколько строк-заглушек показать при `loading`. */\n loadingRows?: number\n /** Сколько колонок занимает служебная строка (пустое состояние и скелетоны). */\n columnCount?: number\n /** Текст пустого состояния. Слот `#empty` сильнее. */\n emptyText?: string\n /** Таблица пуста. По умолчанию определяется по содержимому слота. */\n empty?: boolean\n /** Чередование строк. */\n striped?: boolean\n /** Подсветка строки под курсором. */\n hoverable?: boolean\n /**\n * Прилипающий заголовок: `<thead>` остаётся видимым при вертикальном скролле.\n * Осмысленно вместе с `maxHeight` (иначе таблица не скроллится вертикально).\n */\n stickyHeader?: boolean\n /**\n * Полное число строк набора, включая строки заголовка (`aria-rowcount`).\n *\n * Нужен, когда в DOM не весь набор — например, при виртуализации: диктор\n * считает строки по разметке и объявил бы «5 из 20» на таблице в десять\n * тысяч. Строки при этом обязаны нести `aria-rowindex`.\n */\n rowCount?: number\n /**\n * Фиксированная раскладка (`table-layout: fixed`): ширины колонок берутся из\n * первой строки, а не из содержимого всех.\n *\n * Обязателен, если в DOM не весь набор: иначе ширины считаются по\n * отрисованному окну и прыгают на каждой прокрутке.\n *\n * Класс — arbitrary-значение, а не `table-fixed`: такого правила в\n * `presetMini` нет, и класс молча не превратился бы в CSS.\n */\n fixedLayout?: boolean\n /**\n * Минимальная ширина самой таблицы (число — пиксели).\n *\n * Нужна при `fixedLayout`: с фиксированной раскладкой и шириной `auto`\n * браузер вписывает таблицу в контейнер и делит место между колонками\n * пропорционально — то есть заданные ширины молча ужимаются, а\n * горизонтальной прокрутки, на которой держатся закреплённые колонки, не\n * возникает вовсе.\n */\n tableMinWidth?: string | number\n /**\n * Максимальная высота скролл-контейнера (включает вертикальный скролл).\n * Число трактуется как пиксели. Нужен для работы `stickyHeader`.\n */\n maxHeight?: string | number\n}\n\n/**\n * `GrTable` — «тонкий» GR-контейнер для табличных данных.\n *\n * Рендерит `<table>` внутри скролл-обёртки и даёт слоты `#caption`, `#header`,\n * default (tbody) и `#footer`. Стили ячеек (`<th>`/`<td>` паддинги, выравнивание)\n * оставлены на консьюмере.\n *\n * ВАЖНО: `min-w-full` сам по себе не даст горизонтального скролла. Для него\n * консьюмеру нужны `white-space: nowrap` на ячейках или явные ширины колонок.\n */\nconst props = withDefaults(defineProps<GrTableProps>(), {\n size: undefined,\n caption: undefined,\n ariaLabel: undefined,\n ariaLabelledby: undefined,\n regionLabel: undefined,\n loading: false,\n loadingRows: 3,\n columnCount: 1,\n emptyText: undefined,\n empty: undefined,\n striped: false,\n hoverable: false,\n stickyHeader: false,\n maxHeight: undefined,\n rowCount: undefined,\n fixedLayout: false,\n tableMinWidth: undefined,\n})\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTable' })\nconst tableTextClass = computed(() => tableSizes[resolvedSize.value])\nconst hasCaption = computed(() => Boolean(slots.caption) || Boolean(props.caption))\n\nconst { t } = useGranularityTranslations()\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.table.empty', 'Nothing here yet'))\n\n// Пустоту видно по слоту: `v-for` по пустому массиву оставляет фрагмент без\n// узлов, `v-if` — комментарий, и ни то ни другое строкой не является.\nconst isEmpty = computed(() => {\n if (props.empty !== undefined)\n return props.empty\n\n return !hasMeaningfulSlotContent(slots.default?.() ?? [])\n})\n\nconst showRows = computed(() => !props.loading && !isEmpty.value)\nconst loadingRowCount = computed(() => Math.max(1, Math.trunc(props.loadingRows)))\nconst serviceColSpan = computed(() => Math.max(1, Math.trunc(props.columnCount)))\n\nconst tbodyClass = computed(() => [\n 'text-[var(--gr-fg)]',\n props.striped && showRows.value ? stripedClass : '',\n props.hoverable && showRows.value ? hoverableClass : '',\n].filter(Boolean).join(' '))\n\nconst scrollStyle = computed(() => {\n // Неполный набор в DOM (`rowCount`) означает виртуализацию, а с ней обязан\n // быть выключен браузерный scroll anchoring: он подправляет `scrollTop`,\n // когда меняется высота содержимого выше видимого узла, — а окно меняет её\n // на каждом кадре прокрутки, и список уезжает.\n const anchor = props.rowCount === undefined ? undefined : { overflowAnchor: 'none' as const }\n\n if (props.maxHeight === undefined)\n return anchor\n\n const value = typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight\n return { ...anchor, maxHeight: value, overflowY: 'auto' as const }\n})\n\n// Прилипающий заголовок: `<thead>` на `position: sticky`; фон обязателен, иначе\n// сквозь него будут просвечивать строки при скролле.\nconst theadClass = computed(() => [\n 'bg-[var(--gr-muted)] text-[var(--gr-muted-fg)]',\n props.stickyHeader ? 'sticky top-0 z-[1]' : '',\n].filter(Boolean).join(' '))\n\nconst tableStyle = computed(() => {\n if (props.tableMinWidth === undefined)\n return undefined\n\n const value = typeof props.tableMinWidth === 'number' ? `${props.tableMinWidth}px` : props.tableMinWidth\n\n return { minWidth: value }\n})\n\nconst scrollEl = ref<HTMLElement | null>(null)\n\nfunction scrollTo(options: ScrollToOptions): void {\n scrollEl.value?.scrollTo(options)\n}\n\n// Только верхний уровень собственного tbody: вложенная таблица в ячейке не\n// должна сдвигать индексацию строк.\nfunction contentRows(): HTMLElement[] {\n if (!showRows.value)\n return []\n const el = scrollEl.value\n if (!el)\n return []\n return Array.from(el.querySelectorAll<HTMLElement>(':scope > table > tbody > tr'))\n}\n\nfunction scrollToRow(index: number, options?: ScrollIntoViewOptions): boolean {\n const row = contentRows()[index]\n if (!row)\n return false\n row.scrollIntoView(options ?? { block: 'nearest' })\n return true\n}\n\ndefineExpose({\n /** Прокрутить скролл-контейнер таблицы — тот же контракт, что у `GrDataTable`. */\n scrollTo,\n /** Прокрутить к строке содержимого по её индексу в разметке. `false` — строки нет в DOM. */\n scrollToRow,\n})\n\ndefineSlots<{\n /** Строки таблицы, когда разметка пишется руками вместо `data`. */\n default?: () => any\n /** Подпись таблицы — `<caption>`, читается диктором первой. */\n caption?: () => any\n /** Шапка вместо построенной по `columns`. */\n header?: () => any\n /** Содержимое, пока едут данные, — вместо строк-заглушек. */\n loading?: () => any\n /** Пустое состояние вместо текста по умолчанию. */\n empty?: () => any\n /** Итоговая строка под таблицей. */\n footer?: () => any\n}>()\n</script>\n\n<template>\n <div\n ref=\"scrollEl\"\n data-gr-table-scroll\n :role=\"regionLabel ? 'region' : undefined\"\n :aria-label=\"regionLabel\"\n :aria-busy=\"loading ? 'true' : undefined\"\n tabindex=\"0\"\n class=\"overflow-x-auto rounded-[var(--gr-radius-lg)] border border-[var(--gr-brd)] bg-[var(--gr-card)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n :style=\"scrollStyle\"\n >\n <table\n data-gr-table\n class=\"min-w-full\" :class=\"[tableTextClass, fixedLayout ? '[table-layout:fixed]' : '']\"\n :style=\"tableStyle\"\n :aria-label=\"ariaLabelledby ? undefined : ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :aria-rowcount=\"rowCount\"\n >\n <caption v-if=\"hasCaption\" class=\"sr-only\">\n <slot name=\"caption\">\n{{ caption }}\n</slot>\n </caption>\n <thead :class=\"theadClass\">\n <slot name=\"header\" />\n </thead>\n <tbody :class=\"tbodyClass\">\n <template v-if=\"loading\">\n <slot name=\"loading\">\n <tr v-for=\"row in loadingRowCount\" :key=\"row\" data-gr-table-loading-row>\n <td :colspan=\"serviceColSpan\" :class=\"loadingRowCellClass\">\n <GrSkeleton />\n </td>\n </tr>\n </slot>\n </template>\n\n <tr v-else-if=\"isEmpty\" data-gr-table-empty>\n <td :colspan=\"serviceColSpan\" :class=\"emptyCellClass\">\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </td>\n </tr>\n\n <slot v-else />\n </tbody>\n <tfoot v-if=\"slots.footer\" class=\"text-[var(--gr-fg)]\">\n <slot name=\"footer\" />\n </tfoot>\n </table>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref, useSlots } from 'vue'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\nimport { hasMeaningfulSlotContent } from '../shared/slotNodes'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport {\n emptyCellClass,\n hoverableClass,\n loadingRowCellClass,\n stripedClass,\n tableSizes,\n type GrTableSize,\n} from './grTableStyles'\n\nexport interface GrTableProps {\n /**\n * Размер таблицы — базовый кегль текста. Паддинги ячеек оставлены за\n * консьюмером (GrTable — «тонкий» контейнер).\n */\n size?: GrTableSize\n /**\n * Текст caption для screen reader'ов. Рендерится как `<caption class=\"sr-only\">`,\n * если не передан слот `#caption`.\n */\n caption?: string\n /** Прямой ARIA-label для `<table>`. Игнорируется, если задан `ariaLabelledby`. */\n ariaLabel?: string\n /** ID элемента-заголовка, связанного с `<table>` через `aria-labelledby`. */\n ariaLabelledby?: string\n /**\n * ARIA-label для скролл-контейнера. Включает `role=\"region\"`; сам скролл\n * достижим с клавиатуры всегда, независимо от метки.\n */\n regionLabel?: string\n /** Идёт загрузка: вместо строк — скелетоны, контейнер помечен `aria-busy`. */\n loading?: boolean\n /** Сколько строк-заглушек показать при `loading`. */\n loadingRows?: number\n /** Сколько колонок занимает служебная строка (пустое состояние и скелетоны). */\n columnCount?: number\n /** Текст пустого состояния. Слот `#empty` сильнее. */\n emptyText?: string\n /** Таблица пуста. По умолчанию определяется по содержимому слота. */\n empty?: boolean\n /** Чередование строк. */\n striped?: boolean\n /** Подсветка строки под курсором. */\n hoverable?: boolean\n /**\n * Прилипающий заголовок: `<thead>` остаётся видимым при вертикальном скролле.\n * Осмысленно вместе с `maxHeight` (иначе таблица не скроллится вертикально).\n */\n stickyHeader?: boolean\n /**\n * Полное число строк набора, включая строки заголовка (`aria-rowcount`).\n *\n * Нужен, когда в DOM не весь набор — например, при виртуализации: диктор\n * считает строки по разметке и объявил бы «5 из 20» на таблице в десять\n * тысяч. Строки при этом обязаны нести `aria-rowindex`.\n */\n rowCount?: number\n /**\n * Фиксированная раскладка (`table-layout: fixed`): ширины колонок берутся из\n * первой строки, а не из содержимого всех.\n *\n * Обязателен, если в DOM не весь набор: иначе ширины считаются по\n * отрисованному окну и прыгают на каждой прокрутке.\n *\n * Класс — arbitrary-значение, а не `table-fixed`: такого правила в\n * `presetMini` нет, и класс молча не превратился бы в CSS.\n */\n fixedLayout?: boolean\n /**\n * Минимальная ширина самой таблицы (число — пиксели).\n *\n * Нужна при `fixedLayout`: с фиксированной раскладкой и шириной `auto`\n * браузер вписывает таблицу в контейнер и делит место между колонками\n * пропорционально — то есть заданные ширины молча ужимаются, а\n * горизонтальной прокрутки, на которой держатся закреплённые колонки, не\n * возникает вовсе.\n */\n tableMinWidth?: string | number\n /**\n * Максимальная высота скролл-контейнера (включает вертикальный скролл).\n * Число трактуется как пиксели. Нужен для работы `stickyHeader`.\n */\n maxHeight?: string | number\n}\n\n/**\n * `GrTable` — «тонкий» GR-контейнер для табличных данных.\n *\n * Рендерит `<table>` внутри скролл-обёртки и даёт слоты `#caption`, `#header`,\n * default (tbody) и `#footer`. Стили ячеек (`<th>`/`<td>` паддинги, выравнивание)\n * оставлены на консьюмере.\n *\n * ВАЖНО: `min-w-full` сам по себе не даст горизонтального скролла. Для него\n * консьюмеру нужны `white-space: nowrap` на ячейках или явные ширины колонок.\n */\nconst props = withDefaults(defineProps<GrTableProps>(), {\n size: undefined,\n caption: undefined,\n ariaLabel: undefined,\n ariaLabelledby: undefined,\n regionLabel: undefined,\n loading: false,\n loadingRows: 3,\n columnCount: 1,\n emptyText: undefined,\n empty: undefined,\n striped: false,\n hoverable: false,\n stickyHeader: false,\n maxHeight: undefined,\n rowCount: undefined,\n fixedLayout: false,\n tableMinWidth: undefined,\n})\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTable' })\nconst tableTextClass = computed(() => tableSizes[resolvedSize.value])\nconst hasCaption = computed(() => Boolean(slots.caption) || Boolean(props.caption))\n\nconst { t } = useGranularityTranslations()\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.table.empty', 'Nothing here yet'))\n\n// Пустоту видно по слоту: `v-for` по пустому массиву оставляет фрагмент без\n// узлов, `v-if` — комментарий, и ни то ни другое строкой не является.\nconst isEmpty = computed(() => {\n if (props.empty !== undefined)\n return props.empty\n\n return !hasMeaningfulSlotContent(slots.default?.() ?? [])\n})\n\nconst showRows = computed(() => !props.loading && !isEmpty.value)\nconst loadingRowCount = computed(() => Math.max(1, Math.trunc(props.loadingRows)))\nconst serviceColSpan = computed(() => Math.max(1, Math.trunc(props.columnCount)))\n\nconst tbodyClass = computed(() => [\n 'text-[var(--gr-fg)]',\n props.striped && showRows.value ? stripedClass : '',\n props.hoverable && showRows.value ? hoverableClass : '',\n].filter(Boolean).join(' '))\n\nconst scrollStyle = computed(() => {\n // Неполный набор в DOM (`rowCount`) означает виртуализацию, а с ней обязан\n // быть выключен браузерный scroll anchoring: он подправляет `scrollTop`,\n // когда меняется высота содержимого выше видимого узла, — а окно меняет её\n // на каждом кадре прокрутки, и список уезжает.\n const anchor = props.rowCount === undefined ? undefined : { overflowAnchor: 'none' as const }\n\n if (props.maxHeight === undefined)\n return anchor\n\n const value = typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight\n return { ...anchor, maxHeight: value, overflowY: 'auto' as const }\n})\n\n// Прилипающий заголовок: `<thead>` на `position: sticky`; фон обязателен, иначе\n// сквозь него будут просвечивать строки при скролле.\nconst theadClass = computed(() => [\n 'bg-[var(--gr-muted)] text-[var(--gr-muted-fg)]',\n props.stickyHeader ? 'sticky top-0 z-[1]' : '',\n].filter(Boolean).join(' '))\n\nconst tableStyle = computed(() => {\n if (props.tableMinWidth === undefined)\n return undefined\n\n const value = typeof props.tableMinWidth === 'number' ? `${props.tableMinWidth}px` : props.tableMinWidth\n\n return { minWidth: value }\n})\n\nconst scrollEl = ref<HTMLElement | null>(null)\n\nfunction scrollTo(options: ScrollToOptions): void {\n scrollEl.value?.scrollTo(options)\n}\n\n// Только верхний уровень собственного tbody: вложенная таблица в ячейке не\n// должна сдвигать индексацию строк.\nfunction contentRows(): HTMLElement[] {\n if (!showRows.value)\n return []\n const el = scrollEl.value\n if (!el)\n return []\n return Array.from(el.querySelectorAll<HTMLElement>(':scope > table > tbody > tr'))\n}\n\nfunction scrollToRow(index: number, options?: ScrollIntoViewOptions): boolean {\n const row = contentRows()[index]\n if (!row)\n return false\n row.scrollIntoView(options ?? { block: 'nearest' })\n return true\n}\n\ndefineExpose({\n /** Прокрутить скролл-контейнер таблицы — тот же контракт, что у `GrDataTable`. */\n scrollTo,\n /** Прокрутить к строке содержимого по её индексу в разметке. `false` — строки нет в DOM. */\n scrollToRow,\n})\n\ndefineSlots<{\n /** Строки таблицы, когда разметка пишется руками вместо `data`. */\n default?: () => any\n /** Подпись таблицы — `<caption>`, читается диктором первой. */\n caption?: () => any\n /** Шапка вместо построенной по `columns`. */\n header?: () => any\n /** Содержимое, пока едут данные, — вместо строк-заглушек. */\n loading?: () => any\n /** Пустое состояние вместо текста по умолчанию. */\n empty?: () => any\n /** Итоговая строка под таблицей. */\n footer?: () => any\n}>()\n</script>\n\n<template>\n <div\n ref=\"scrollEl\"\n data-gr-table-scroll\n :role=\"regionLabel ? 'region' : undefined\"\n :aria-label=\"regionLabel\"\n :aria-busy=\"loading ? 'true' : undefined\"\n tabindex=\"0\"\n class=\"overflow-x-auto rounded-[var(--gr-radius-lg)] border border-[var(--gr-brd)] bg-[var(--gr-card)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n :style=\"scrollStyle\"\n >\n <table\n data-gr-table\n class=\"min-w-full\" :class=\"[tableTextClass, fixedLayout ? '[table-layout:fixed]' : '']\"\n :style=\"tableStyle\"\n :aria-label=\"ariaLabelledby ? undefined : ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :aria-rowcount=\"rowCount\"\n >\n <caption v-if=\"hasCaption\" class=\"sr-only\">\n <slot name=\"caption\">\n{{ caption }}\n</slot>\n </caption>\n <thead :class=\"theadClass\">\n <slot name=\"header\" />\n </thead>\n <tbody :class=\"tbodyClass\">\n <template v-if=\"loading\">\n <slot name=\"loading\">\n <tr v-for=\"row in loadingRowCount\" :key=\"row\" data-gr-table-loading-row>\n <td :colspan=\"serviceColSpan\" :class=\"loadingRowCellClass\">\n <GrSkeleton />\n </td>\n </tr>\n </slot>\n </template>\n\n <tr v-else-if=\"isEmpty\" data-gr-table-empty>\n <td :colspan=\"serviceColSpan\" :class=\"emptyCellClass\">\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </td>\n </tr>\n\n <slot v-else />\n </tbody>\n <tfoot v-if=\"slots.footer\" class=\"text-[var(--gr-fg)]\">\n <slot name=\"footer\" />\n </tfoot>\n </table>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrTabs-ugDnkk-2.js","names":[],"sources":["../../../../src/components/GrTabs/GrTabs.vue","../../../../src/components/GrTabs/GrTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch, type Component } from 'vue'\n\nimport IconX from '~icons/lucide/x'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { iconClass, iconTag } from '../shared/icon'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n grTabsBadgeClass,\n grTabsCloseClass,\n grTabsEmptyClass,\n grTabsListClass,\n grTabsTabClass,\n tabCloseIconSizes,\n tabContentClass,\n tabIconClass,\n type GrTabsOrientation,\n type GrTabsSize,\n type GrTabsVariant,\n} from './grTabsStyles'\n\nexport interface GrTab {\n value: string\n label: string\n badge?: string\n /**\n * Иконка слева от подписи: Vue-компонент либо класс иконки вашей UnoCSS-сборки\n * (`'i-lucide-user'` — тогда нужен ваш `presetIcons`, см. `docs/installation.md`).\n */\n icon?: string | Component\n disabled?: boolean\n /** Перекрывает `closable` ряда в обе стороны: закрепить вкладку или закрыть одну её. */\n closable?: boolean\n}\n\nexport interface GrTabsProps {\n modelValue: string\n tabs: GrTab[]\n /**\n * База id для ARIA-связки с `GrTabPanels`. Если задана, каждая вкладка\n * получает `id=\"<idBase>-tab-<value>\"` и `aria-controls=\"<idBase>-panel-<value>\"`.\n * Передайте тот же `idBase` в `GrTabPanels`, чтобы связать `tab`↔`tabpanel`.\n */\n idBase?: string\n size?: GrTabsSize\n /** Вид ряда: обойма с таблетками или ряд с подчёркиванием. */\n variant?: GrTabsVariant\n /**\n * `automatic` (по умолчанию) — стрелка сразу переключает вкладку;\n * `manual` — стрелка двигает только фокус, выбор подтверждается\n * `Enter`/`Space`. Второй режим для вкладок с тяжёлой загрузкой: перебор\n * стрелками иначе тянет каждую панель.\n */\n activationMode?: GrTabsActivationMode\n /** Горизонтальный (по умолчанию) или вертикальный список вкладок. */\n orientation?: GrTabsOrientation\n /**\n * Крестик на вкладках и закрытие по `Delete`/`Backspace`. Точечно снимается\n * `closable: false` у самой вкладки.\n */\n closable?: boolean\n /** Текст, когда список вкладок пуст. Слот `#empty` сильнее. */\n emptyText?: string\n}\n\nexport interface GrTabsEmits {\n (e: 'update:modelValue', value: string): void\n /**\n * Просьба закрыть вкладку. Компонент список не трогает: `tabs` — проп, и\n * закрытие может не состояться («сохранить изменения?»).\n */\n (e: 'close', value: string): void\n}\n\nexport type GrTabsActivationMode = 'automatic' | 'manual'\n\nexport type { GrTabsOrientation, GrTabsVariant } from './grTabsStyles'\n\n/**\n * GrTabs — горизонтальная группа вкладок с паттерном WAI-ARIA `tablist`.\n *\n * A11y:\n * - `role=\"tablist\"` на корне, `role=\"tab\"` на каждой вкладке.\n * - Roving `tabindex`: активная вкладка `0`, остальные `-1`.\n * - Клавиатура: `ArrowLeft`/`ArrowRight` — цикличный переход, `Home`/`End` — к первой/последней.\n * - При переключении стрелками DOM-фокус программно переносится на новую вкладку.\n *\n * Сам компонент не рендерит `tabpanel` — это ответственность консьюмера (по `aria-controls`/внешней разметке).\n */\nconst props = withDefaults(defineProps<GrTabsProps>(), {\n idBase: undefined,\n size: undefined,\n // Дефолт живёт в резолвере: Vue подставил бы свой раньше, чем компонент\n // заглянет в `GrConfigProvider`.\n variant: undefined,\n activationMode: 'automatic',\n orientation: 'horizontal',\n closable: false,\n emptyText: undefined,\n})\n\ndefineSlots<{\n /** Содержимое вкладки целиком — вместо подписи, иконки и счётчика. Крестик остаётся. */\n tab?: (props: { tab: GrTab, active: boolean, disabled: boolean }) => unknown\n /** Пустой ряд — вместо текста из локали. */\n empty?: () => unknown\n}>()\n\nconst emit = defineEmits<GrTabsEmits>()\n\nconst { t } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTabs' })\nconst resolvedVariant = useGrComponentProp('GrTabs', 'variant', () => props.variant, 'pills')\n\nconst tablistClass = computed(() => grTabsListClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n orientation: props.orientation,\n}))\n\nfunction tabClass(tab: GrTab): string {\n return grTabsTabClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n active: tab.value === props.modelValue,\n disabled: Boolean(tab.disabled),\n })\n}\n\nconst badgeClass = computed(() => grTabsBadgeClass(resolvedSize.value))\nconst closeClass = computed(() => grTabsCloseClass(resolvedSize.value))\nconst closeIconClass = computed(() => tabCloseIconSizes[resolvedSize.value])\nconst emptyClass = computed(() => grTabsEmptyClass(resolvedSize.value))\n\nconst isEmpty = computed(() => props.tabs.length === 0)\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.tabs.empty', 'Nothing here yet'))\nconst closeTitle = computed(() => t('gr.tabs.close', 'Close tab'))\n\n/** Отключённая вкладка не принимает взаимодействия, а закрытие — тоже взаимодействие. */\nfunction isClosable(tab: GrTab): boolean {\n return !tab.disabled && (tab.closable ?? props.closable)\n}\n\nconst buttonRefs = ref<HTMLButtonElement[]>([])\n\n/**\n * Vue зовёт ref-функцию с `null`, когда узел исчез. Игнорировать такой вызов\n * нельзя: в массиве оставались отсоединённые от DOM кнопки, и `focus()` по\n * сократившемуся списку молча проваливался в `<body>`.\n */\nfunction setButtonRef(el: unknown, index: number): void {\n if (el instanceof HTMLButtonElement)\n buttonRefs.value[index] = el\n else\n delete buttonRefs.value[index]\n\n if (buttonRefs.value.length > props.tabs.length)\n buttonRefs.value.length = props.tabs.length\n}\n\nconst activeIndex = computed(() => props.tabs.findIndex(t => t.value === props.modelValue))\n\nfunction isEnabled(tab: GrTab): boolean {\n return !tab.disabled\n}\n\nconst tabIndexes = computed(() => props.tabs.map((_, index) => index))\n\n/**\n * Кольцо roving-фокуса. При `activationMode: 'manual'` фокус уезжает вперёд\n * выбора — это и есть разница между `activeKey` примитива и `initialKey`:\n * первый ведёт фокус, второй подставляет выбранную вкладку, пока фокус не\n * трогали.\n *\n * Выбор стрелка **не** переносит: этим заведует `onKeydown` по\n * `activationMode`. Через `onMove` это выразить нельзя — клик тоже зовёт\n * `focusKey`, и выбор эмитился бы дважды.\n */\nconst roving = useRovingFocus<number>({\n items: () => tabIndexes.value,\n elementFor: index => buttonRefs.value[index],\n isDisabled: index => Boolean(props.tabs[index]?.disabled),\n orientation: () => (props.orientation === 'vertical' ? 'vertical' : 'horizontal'),\n skipDisabled: () => true,\n /**\n * Пока фокус не трогали, остановку `Tab` держит выбранная вкладка. Если\n * `modelValue` не совпал ни с одной (пустое начальное значение, асинхронный\n * список, удалённая активная вкладка), примитив ставит её на первую\n * доступную — иначе tablist выпал бы из обхода целиком и молча.\n *\n * Выключенность здесь не проверяется намеренно: на кнопке `aria-disabled`, а\n * не нативный `disabled`, поэтому выключенная вкладка фокусируется и держать\n * остановку вправе.\n */\n initialKey: () => (activeIndex.value >= 0 ? activeIndex.value : undefined),\n /**\n * Прокрутка до фокуса, а не после: иначе браузер доскроллит сам по-своему, и\n * ряд дёрнется дважды. Синхронно — фокус обязан остаться в том же такте, что\n * и раньше: тик, где он нужен, добавляет `focusIndex`.\n */\n beforeFocus: index => scrollIntoView(index),\n})\n\n/**\n * Фокус после перерисовки — для путей, где список только что изменился\n * (закрытие вкладки). Клавиатура идёт через примитив напрямую: там ряд уже\n * отрисован, и лишний тик только отложил бы фокус.\n */\nasync function focusIndex(index: number): Promise<void> {\n await nextTick()\n await roving.focusKey(index)\n}\n\n/**\n * Ряд прокручивается, а не переносится, поэтому активная вкладка может уехать\n * за край — например, когда её выбрали снаружи. `scrollIntoView` нет в jsdom и\n * в старых движках, поэтому вызов необязательный.\n */\nfunction scrollIntoView(index: number): void {\n buttonRefs.value[index]?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })\n}\n\nwatch(() => props.modelValue, async () => {\n await nextTick()\n if (activeIndex.value >= 0)\n scrollIntoView(activeIndex.value)\n})\n\nasync function selectByIndex(index: number, focus = false): Promise<void> {\n const tab = props.tabs[index]\n if (!tab || !isEnabled(tab))\n return\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n if (focus)\n await focusIndex(index)\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (props.tabs.length === 0)\n return\n\n const currentIndex = roving.rovingKey.value ?? 0\n\n switch (event.key) {\n case 'Enter':\n case ' ':\n // В ручном режиме выбор подтверждается явно — на той вкладке, где фокус.\n if (props.activationMode === 'manual') {\n event.preventDefault()\n void selectByIndex(currentIndex, false)\n }\n return\n case 'Delete':\n case 'Backspace': {\n // Единственный клавиатурный путь к закрытию: крестик в таб-порядке не стоит.\n const tab = props.tabs[currentIndex]\n if (tab && isClosable(tab)) {\n event.preventDefault()\n requestClose(currentIndex)\n }\n return\n }\n }\n\n // Стрелки, `Home`, `End` — фокус ведёт примитив по объявленной ориентации.\n if (!roving.handleNavigationKeys(event))\n return\n\n // `automatic`: выбор едет за фокусом. `manual`: ждёт `Enter`/`Space`.\n if (props.activationMode !== 'manual')\n void selectByIndex(roving.rovingKey.value ?? currentIndex, false)\n}\n\n/**\n * Закрытая вкладка, которая держала фокус. Компонент список не укорачивает —\n * это делает потребитель, — поэтому фокус возвращается только после того, как\n * `tabs` действительно стал короче.\n */\nlet pendingClose: { index: number, value: string } | null = null\n\nfunction requestClose(index: number): void {\n const tab = props.tabs[index]\n if (!tab || !isClosable(tab))\n return\n\n const held = buttonRefs.value[index] === document.activeElement\n pendingClose = held ? { index, value: tab.value } : null\n\n emit('close', tab.value)\n}\n\n// Без этого клавиатурное закрытие роняет фокус в `<body>`: кнопка, на которой\n// он стоял, исчезает из DOM вместе со вкладкой.\nwatch(() => props.tabs.length, async (next, prev) => {\n const pending = pendingClose\n pendingClose = null\n\n if (!pending || next >= prev)\n return\n // Ушла не та вкладка — потребитель поменял список по своей причине.\n if (props.tabs.some(tab => tab.value === pending.value))\n return\n\n await nextTick()\n\n const target = Math.min(pending.index, next - 1)\n if (target >= 0)\n void focusIndex(target)\n else\n roving.reset()\n})\n\nfunction onClick(event: MouseEvent, tab: GrTab, index: number): void {\n if (!isEnabled(tab))\n return\n\n // Крестик — не кнопка, поэтому его клик приходит сюда же и разбирается по цели.\n if (event.target instanceof Element && event.target.closest('[data-gr-tab-close]')) {\n requestClose(index)\n return\n }\n\n roving.setActive(index)\n\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n}\n</script>\n\n<template>\n <!--\n Пустой ряд — не пустой `tablist`: роль обязана владеть потомками `tab`,\n и текст внутри неё был бы нарушением, а не пустым состоянием.\n -->\n <div v-if=\"isEmpty\" data-gr-tabs-empty :class=\"emptyClass\">\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <div\n v-else\n role=\"tablist\"\n data-gr-tabs\n :aria-orientation=\"orientation\"\n :class=\"tablistClass\"\n @keydown=\"onKeydown\"\n >\n <button\n v-for=\"(tab, index) in tabs\"\n :id=\"idBase ? `${idBase}-tab-${tab.value}` : undefined\"\n :key=\"tab.value\"\n :ref=\"el => setButtonRef(el, index)\"\n type=\"button\"\n role=\"tab\"\n data-gr-tab\n :aria-controls=\"idBase ? `${idBase}-panel-${tab.value}` : undefined\"\n :aria-selected=\"tab.value === modelValue ? 'true' : 'false'\"\n :aria-disabled=\"tab.disabled ? 'true' : undefined\"\n :aria-keyshortcuts=\"isClosable(tab) ? 'Delete' : undefined\"\n :tabindex=\"roving.tabindexFor(index)\"\n :class=\"tabClass(tab)\"\n @click=\"onClick($event, tab, index)\"\n >\n <span :class=\"tabContentClass\">\n <slot\n name=\"tab\"\n :tab=\"tab\"\n :active=\"tab.value === modelValue\"\n :disabled=\"Boolean(tab.disabled)\"\n >\n <component\n :is=\"iconTag(tab.icon)\"\n v-if=\"tab.icon\"\n :class=\"[tabIconClass, iconClass(tab.icon)]\"\n aria-hidden=\"true\"\n />\n <span>{{ tab.label }}</span>\n <span v-if=\"tab.badge\" :class=\"badgeClass\">\n {{ tab.badge }}\n </span>\n </slot>\n\n <!-- Снаружи слота: своя разметка вкладки не должна отнимать закрытие. -->\n <span\n v-if=\"isClosable(tab)\"\n data-gr-tab-close\n :class=\"closeClass\"\n :title=\"closeTitle\"\n aria-hidden=\"true\"\n >\n <IconX :class=\"closeIconClass\" />\n </span>\n </span>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch, type Component } from 'vue'\n\nimport IconX from '~icons/lucide/x'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { iconClass, iconTag } from '../shared/icon'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n grTabsBadgeClass,\n grTabsCloseClass,\n grTabsEmptyClass,\n grTabsListClass,\n grTabsTabClass,\n tabCloseIconSizes,\n tabContentClass,\n tabIconClass,\n type GrTabsOrientation,\n type GrTabsSize,\n type GrTabsVariant,\n} from './grTabsStyles'\n\nexport interface GrTab {\n value: string\n label: string\n badge?: string\n /**\n * Иконка слева от подписи: Vue-компонент либо класс иконки вашей UnoCSS-сборки\n * (`'i-lucide-user'` — тогда нужен ваш `presetIcons`, см. `docs/installation.md`).\n */\n icon?: string | Component\n disabled?: boolean\n /** Перекрывает `closable` ряда в обе стороны: закрепить вкладку или закрыть одну её. */\n closable?: boolean\n}\n\nexport interface GrTabsProps {\n modelValue: string\n tabs: GrTab[]\n /**\n * База id для ARIA-связки с `GrTabPanels`. Если задана, каждая вкладка\n * получает `id=\"<idBase>-tab-<value>\"` и `aria-controls=\"<idBase>-panel-<value>\"`.\n * Передайте тот же `idBase` в `GrTabPanels`, чтобы связать `tab`↔`tabpanel`.\n */\n idBase?: string\n size?: GrTabsSize\n /** Вид ряда: обойма с таблетками или ряд с подчёркиванием. */\n variant?: GrTabsVariant\n /**\n * `automatic` (по умолчанию) — стрелка сразу переключает вкладку;\n * `manual` — стрелка двигает только фокус, выбор подтверждается\n * `Enter`/`Space`. Второй режим для вкладок с тяжёлой загрузкой: перебор\n * стрелками иначе тянет каждую панель.\n */\n activationMode?: GrTabsActivationMode\n /** Горизонтальный (по умолчанию) или вертикальный список вкладок. */\n orientation?: GrTabsOrientation\n /**\n * Крестик на вкладках и закрытие по `Delete`/`Backspace`. Точечно снимается\n * `closable: false` у самой вкладки.\n */\n closable?: boolean\n /** Текст, когда список вкладок пуст. Слот `#empty` сильнее. */\n emptyText?: string\n}\n\nexport interface GrTabsEmits {\n (e: 'update:modelValue', value: string): void\n /**\n * Просьба закрыть вкладку. Компонент список не трогает: `tabs` — проп, и\n * закрытие может не состояться («сохранить изменения?»).\n */\n (e: 'close', value: string): void\n}\n\nexport type GrTabsActivationMode = 'automatic' | 'manual'\n\nexport type { GrTabsOrientation, GrTabsVariant } from './grTabsStyles'\n\n/**\n * GrTabs — горизонтальная группа вкладок с паттерном WAI-ARIA `tablist`.\n *\n * A11y:\n * - `role=\"tablist\"` на корне, `role=\"tab\"` на каждой вкладке.\n * - Roving `tabindex`: активная вкладка `0`, остальные `-1`.\n * - Клавиатура: `ArrowLeft`/`ArrowRight` — цикличный переход, `Home`/`End` — к первой/последней.\n * - При переключении стрелками DOM-фокус программно переносится на новую вкладку.\n *\n * Сам компонент не рендерит `tabpanel` — это ответственность консьюмера (по `aria-controls`/внешней разметке).\n */\nconst props = withDefaults(defineProps<GrTabsProps>(), {\n idBase: undefined,\n size: undefined,\n // Дефолт живёт в резолвере: Vue подставил бы свой раньше, чем компонент\n // заглянет в `GrConfigProvider`.\n variant: undefined,\n activationMode: 'automatic',\n orientation: 'horizontal',\n closable: false,\n emptyText: undefined,\n})\n\ndefineSlots<{\n /** Содержимое вкладки целиком — вместо подписи, иконки и счётчика. Крестик остаётся. */\n tab?: (props: { tab: GrTab, active: boolean, disabled: boolean }) => unknown\n /** Пустой ряд — вместо текста из локали. */\n empty?: () => unknown\n}>()\n\nconst emit = defineEmits<GrTabsEmits>()\n\nconst { t } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTabs' })\nconst resolvedVariant = useGrComponentProp('GrTabs', 'variant', () => props.variant, 'pills')\n\nconst tablistClass = computed(() => grTabsListClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n orientation: props.orientation,\n}))\n\nfunction tabClass(tab: GrTab): string {\n return grTabsTabClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n active: tab.value === props.modelValue,\n disabled: Boolean(tab.disabled),\n })\n}\n\nconst badgeClass = computed(() => grTabsBadgeClass(resolvedSize.value))\nconst closeClass = computed(() => grTabsCloseClass(resolvedSize.value))\nconst closeIconClass = computed(() => tabCloseIconSizes[resolvedSize.value])\nconst emptyClass = computed(() => grTabsEmptyClass(resolvedSize.value))\n\nconst isEmpty = computed(() => props.tabs.length === 0)\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.tabs.empty', 'Nothing here yet'))\nconst closeTitle = computed(() => t('gr.tabs.close', 'Close tab'))\n\n/** Отключённая вкладка не принимает взаимодействия, а закрытие — тоже взаимодействие. */\nfunction isClosable(tab: GrTab): boolean {\n return !tab.disabled && (tab.closable ?? props.closable)\n}\n\nconst buttonRefs = ref<HTMLButtonElement[]>([])\n\n/**\n * Vue зовёт ref-функцию с `null`, когда узел исчез. Игнорировать такой вызов\n * нельзя: в массиве оставались отсоединённые от DOM кнопки, и `focus()` по\n * сократившемуся списку молча проваливался в `<body>`.\n */\nfunction setButtonRef(el: unknown, index: number): void {\n if (el instanceof HTMLButtonElement)\n buttonRefs.value[index] = el\n else\n delete buttonRefs.value[index]\n\n if (buttonRefs.value.length > props.tabs.length)\n buttonRefs.value.length = props.tabs.length\n}\n\nconst activeIndex = computed(() => props.tabs.findIndex(t => t.value === props.modelValue))\n\nfunction isEnabled(tab: GrTab): boolean {\n return !tab.disabled\n}\n\nconst tabIndexes = computed(() => props.tabs.map((_, index) => index))\n\n/**\n * Кольцо roving-фокуса. При `activationMode: 'manual'` фокус уезжает вперёд\n * выбора — это и есть разница между `activeKey` примитива и `initialKey`:\n * первый ведёт фокус, второй подставляет выбранную вкладку, пока фокус не\n * трогали.\n *\n * Выбор стрелка **не** переносит: этим заведует `onKeydown` по\n * `activationMode`. Через `onMove` это выразить нельзя — клик тоже зовёт\n * `focusKey`, и выбор эмитился бы дважды.\n */\nconst roving = useRovingFocus<number>({\n items: () => tabIndexes.value,\n elementFor: index => buttonRefs.value[index],\n isDisabled: index => Boolean(props.tabs[index]?.disabled),\n orientation: () => (props.orientation === 'vertical' ? 'vertical' : 'horizontal'),\n skipDisabled: () => true,\n /**\n * Пока фокус не трогали, остановку `Tab` держит выбранная вкладка. Если\n * `modelValue` не совпал ни с одной (пустое начальное значение, асинхронный\n * список, удалённая активная вкладка), примитив ставит её на первую\n * доступную — иначе tablist выпал бы из обхода целиком и молча.\n *\n * Выключенность здесь не проверяется намеренно: на кнопке `aria-disabled`, а\n * не нативный `disabled`, поэтому выключенная вкладка фокусируется и держать\n * остановку вправе.\n */\n initialKey: () => (activeIndex.value >= 0 ? activeIndex.value : undefined),\n /**\n * Прокрутка до фокуса, а не после: иначе браузер доскроллит сам по-своему, и\n * ряд дёрнется дважды. Синхронно — фокус обязан остаться в том же такте, что\n * и раньше: тик, где он нужен, добавляет `focusIndex`.\n */\n beforeFocus: index => scrollIntoView(index),\n})\n\n/**\n * Фокус после перерисовки — для путей, где список только что изменился\n * (закрытие вкладки). Клавиатура идёт через примитив напрямую: там ряд уже\n * отрисован, и лишний тик только отложил бы фокус.\n */\nasync function focusIndex(index: number): Promise<void> {\n await nextTick()\n await roving.focusKey(index)\n}\n\n/**\n * Ряд прокручивается, а не переносится, поэтому активная вкладка может уехать\n * за край — например, когда её выбрали снаружи. `scrollIntoView` нет в jsdom и\n * в старых движках, поэтому вызов необязательный.\n */\nfunction scrollIntoView(index: number): void {\n buttonRefs.value[index]?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })\n}\n\nwatch(() => props.modelValue, async () => {\n await nextTick()\n if (activeIndex.value >= 0)\n scrollIntoView(activeIndex.value)\n})\n\nasync function selectByIndex(index: number, focus = false): Promise<void> {\n const tab = props.tabs[index]\n if (!tab || !isEnabled(tab))\n return\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n if (focus)\n await focusIndex(index)\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (props.tabs.length === 0)\n return\n\n const currentIndex = roving.rovingKey.value ?? 0\n\n switch (event.key) {\n case 'Enter':\n case ' ':\n // В ручном режиме выбор подтверждается явно — на той вкладке, где фокус.\n if (props.activationMode === 'manual') {\n event.preventDefault()\n void selectByIndex(currentIndex, false)\n }\n return\n case 'Delete':\n case 'Backspace': {\n // Единственный клавиатурный путь к закрытию: крестик в таб-порядке не стоит.\n const tab = props.tabs[currentIndex]\n if (tab && isClosable(tab)) {\n event.preventDefault()\n requestClose(currentIndex)\n }\n return\n }\n }\n\n // Стрелки, `Home`, `End` — фокус ведёт примитив по объявленной ориентации.\n if (!roving.handleNavigationKeys(event))\n return\n\n // `automatic`: выбор едет за фокусом. `manual`: ждёт `Enter`/`Space`.\n if (props.activationMode !== 'manual')\n void selectByIndex(roving.rovingKey.value ?? currentIndex, false)\n}\n\n/**\n * Закрытая вкладка, которая держала фокус. Компонент список не укорачивает —\n * это делает потребитель, — поэтому фокус возвращается только после того, как\n * `tabs` действительно стал короче.\n */\nlet pendingClose: { index: number, value: string } | null = null\n\nfunction requestClose(index: number): void {\n const tab = props.tabs[index]\n if (!tab || !isClosable(tab))\n return\n\n const held = buttonRefs.value[index] === document.activeElement\n pendingClose = held ? { index, value: tab.value } : null\n\n emit('close', tab.value)\n}\n\n// Без этого клавиатурное закрытие роняет фокус в `<body>`: кнопка, на которой\n// он стоял, исчезает из DOM вместе со вкладкой.\nwatch(() => props.tabs.length, async (next, prev) => {\n const pending = pendingClose\n pendingClose = null\n\n if (!pending || next >= prev)\n return\n // Ушла не та вкладка — потребитель поменял список по своей причине.\n if (props.tabs.some(tab => tab.value === pending.value))\n return\n\n await nextTick()\n\n const target = Math.min(pending.index, next - 1)\n if (target >= 0)\n void focusIndex(target)\n else\n roving.reset()\n})\n\nfunction onClick(event: MouseEvent, tab: GrTab, index: number): void {\n if (!isEnabled(tab))\n return\n\n // Крестик — не кнопка, поэтому его клик приходит сюда же и разбирается по цели.\n if (event.target instanceof Element && event.target.closest('[data-gr-tab-close]')) {\n requestClose(index)\n return\n }\n\n roving.setActive(index)\n\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n}\n</script>\n\n<template>\n <!--\n Пустой ряд — не пустой `tablist`: роль обязана владеть потомками `tab`,\n и текст внутри неё был бы нарушением, а не пустым состоянием.\n -->\n <div v-if=\"isEmpty\" data-gr-tabs-empty :class=\"emptyClass\">\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <div\n v-else\n role=\"tablist\"\n data-gr-tabs\n :aria-orientation=\"orientation\"\n :class=\"tablistClass\"\n @keydown=\"onKeydown\"\n >\n <button\n v-for=\"(tab, index) in tabs\"\n :id=\"idBase ? `${idBase}-tab-${tab.value}` : undefined\"\n :key=\"tab.value\"\n :ref=\"el => setButtonRef(el, index)\"\n type=\"button\"\n role=\"tab\"\n data-gr-tab\n :aria-controls=\"idBase ? `${idBase}-panel-${tab.value}` : undefined\"\n :aria-selected=\"tab.value === modelValue ? 'true' : 'false'\"\n :aria-disabled=\"tab.disabled ? 'true' : undefined\"\n :aria-keyshortcuts=\"isClosable(tab) ? 'Delete' : undefined\"\n :tabindex=\"roving.tabindexFor(index)\"\n :class=\"tabClass(tab)\"\n @click=\"onClick($event, tab, index)\"\n >\n <span :class=\"tabContentClass\">\n <slot\n name=\"tab\"\n :tab=\"tab\"\n :active=\"tab.value === modelValue\"\n :disabled=\"Boolean(tab.disabled)\"\n >\n <component\n :is=\"iconTag(tab.icon)\"\n v-if=\"tab.icon\"\n :class=\"[tabIconClass, iconClass(tab.icon)]\"\n aria-hidden=\"true\"\n />\n <span>{{ tab.label }}</span>\n <span v-if=\"tab.badge\" :class=\"badgeClass\">\n {{ tab.badge }}\n </span>\n </slot>\n\n <!-- Снаружи слота: своя разметка вкладки не должна отнимать закрытие. -->\n <span\n v-if=\"isClosable(tab)\"\n data-gr-tab-close\n :class=\"closeClass\"\n :title=\"closeTitle\"\n aria-hidden=\"true\"\n >\n <IconX :class=\"closeIconClass\" />\n </span>\n </span>\n </button>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useFocusTrap.js","names":[],"sources":["../../src/composables/useFocusTrap.ts"],"sourcesContent":["import type { Ref } from 'vue'\nimport { nextTick, onScopeDispose, watch } from 'vue'\n\nimport { getFocusableElementsIn, isFocusable } from './internal/focusables'\n\nexport interface UseFocusTrapOptions {\n /**\n * Работает ли ловушка сейчас. Геттер, а не значение: модальное окно держит\n * фокус, только пока оно верхнее в стеке слоёв, — иначе нижнее окно отбирало\n * бы фокус у открытого поверх него.\n */\n active?: () => boolean\n /** Кому отдать фокус при активации. Не задан или не фокусируем — первый в контейнере. */\n initialFocus?: () => HTMLElement | null | undefined\n /**\n * Запасной приёмник фокуса, когда фокусировать внутри нечего: обычно сама\n * панель с `tabindex=\"-1\"`. Без него фокус остался бы снаружи, и Tab увёл бы\n * пользователя на страницу под окном.\n */\n fallbackFocus?: () => HTMLElement | null | undefined\n /**\n * Дополнительные корни, которые считаются частью слоя. Панель селекта,\n * открытого внутри окна, телепортирована в `body` и лежит вне поддерева\n * контейнера — без этого списка ловушка отбирала бы у неё фокус.\n */\n containers?: () => (HTMLElement | null | undefined)[]\n /**\n * Возвращать фокус туда, где он был до активации. По умолчанию — да.\n * Оверлеи пакета выключают: возврат делает стек слоёв по более строгому\n * правилу (только если фокус на момент закрытия ещё внутри слоя).\n */\n restoreFocus?: boolean\n}\n\n/**\n * Ловушка фокуса: пока активна, Tab ходит по кругу внутри слоя, а фокус,\n * утёкший наружу, возвращается обратно.\n *\n * Реализована на `keydown` и `focusin`, **без sentinel-узлов**: распространённый\n * подход с двумя focus-guard кнопками по краям кладёт в разметку интерактивные\n * элементы, а внутрь контейнера с ARIA-ролью их класть нельзя — роль объявляет\n * потомков презентационными (axe: `nested-interactive`).\n *\n * Слой из ловушки не выводится: порядок окон, Esc и `inert` — забота\n * `useOverlayLayer`. Композиция такая:\n *\n * ```ts\n * const layer = useOverlayLayer(open, close, { modal: true, root: panelRef })\n * useFocusTrap(panelRef, {\n * active: () => open.value && isTopmost.value,\n * containers: layer.rootsAbove,\n * })\n * ```\n */\nexport function useFocusTrap(\n container: Ref<HTMLElement | null>,\n options: UseFocusTrapOptions = {},\n): void {\n let previouslyFocused: HTMLElement | null = null\n let listening = false\n // Последний законный держатель фокуса: с него начинается возврат, когда\n // фокус ушёл в никуда (например, кликом по неинтерактивному фону).\n let lastFocusedInside: HTMLElement | null = null\n\n function roots(): HTMLElement[] {\n const extra = options.containers?.() ?? []\n return [container.value, ...extra].filter((root): root is HTMLElement => Boolean(root))\n }\n\n function contains(target: Node | null): boolean {\n if (!target)\n return false\n return roots().some(root => root === target || root.contains(target))\n }\n\n function focusables(): HTMLElement[] {\n return getFocusableElementsIn(roots())\n }\n\n function focusInitial(): void {\n // Фокус уже внутри слоя — его поставили осознанно: диалог наводит на свою\n // кнопку, окно ввода — на поле, и делают это в тот же такт, когда панель\n // появилась. `initialFocus` — дефолт слоя, а не приказ, поэтому такую\n // установку он не перебивает.\n if (contains(document.activeElement))\n return\n\n const requested = options.initialFocus?.()\n if (requested && isFocusable(requested)) {\n requested.focus()\n return\n }\n\n const [first] = focusables()\n if (first) {\n first.focus()\n return\n }\n\n // Фокусировать нечего — забираем фокус на сам слой, иначе он останется\n // снаружи и Tab уведёт пользователя под окно.\n const fallback = options.fallbackFocus?.() ?? container.value\n fallback?.focus()\n }\n\n function onKeydown(event: KeyboardEvent): void {\n if (event.key !== 'Tab')\n return\n if (!contains(document.activeElement))\n return\n\n const items = focusables()\n if (items.length === 0) {\n // Внутри некуда идти — держим фокус на слое, а не отпускаем на страницу.\n event.preventDefault()\n ;(options.fallbackFocus?.() ?? container.value)?.focus()\n return\n }\n\n const first = items[0]\n const last = items[items.length - 1]\n const active = document.activeElement as HTMLElement | null\n const index = active ? items.indexOf(active) : -1\n\n // Фокус на самой панели (`tabindex=\"-1\"`, в списке её нет): Tab обязан\n // войти внутрь, а не выпрыгнуть за слой.\n if (index === -1) {\n event.preventDefault()\n ;(event.shiftKey ? last : first).focus()\n return\n }\n\n if (event.shiftKey && active === first) {\n event.preventDefault()\n last.focus()\n return\n }\n\n if (!event.shiftKey && active === last) {\n event.preventDefault()\n first.focus()\n }\n }\n\n function onFocusIn(event: FocusEvent): void {\n const target = event.target as HTMLElement | null\n\n if (contains(target)) {\n lastFocusedInside = target\n return\n }\n\n // Фокус утёк наружу — программным `focus()`, кликом по фону или обходом\n // через адресную строку. Возвращаем его на последнее законное место.\n if (lastFocusedInside && contains(lastFocusedInside) && isFocusable(lastFocusedInside)) {\n lastFocusedInside.focus()\n return\n }\n\n focusInitial()\n }\n\n function activate(): void {\n if (listening)\n return\n if (typeof document === 'undefined')\n return\n listening = true\n\n previouslyFocused = (document.activeElement as HTMLElement) ?? null\n lastFocusedInside = null\n\n document.addEventListener('keydown', onKeydown, true)\n document.addEventListener('focusin', onFocusIn, true)\n\n // Ждём, пока Vue домонтирует содержимое слоя: до этого фокусировать нечего.\n void nextTick(() => {\n if (!listening)\n return\n focusInitial()\n })\n }\n\n function deactivate(): void {\n if (!listening)\n return\n listening = false\n\n document.removeEventListener('keydown', onKeydown, true)\n document.removeEventListener('focusin', onFocusIn, true)\n\n const target = previouslyFocused\n previouslyFocused = null\n lastFocusedInside = null\n\n if (options.restoreFocus === false || !target)\n return\n if (!target.isConnected)\n return\n target.focus?.()\n }\n\n watch(\n () => Boolean(options.active?.() ?? true) && Boolean(container.value),\n (isActive) => {\n if (isActive)\n activate()\n else deactivate()\n },\n { immediate: true },\n )\n\n onScopeDispose(deactivate)\n}\n"],"mappings":"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"}
|