@feugene/granularity 0.29.0 → 0.30.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/README.md +9 -6
- package/dist/chunks/{config-_jO3q1Le.js → config-3bQeQvqd.js} +1 -1
- package/dist/chunks/{config-_jO3q1Le.js.map → config-3bQeQvqd.js.map} +1 -1
- package/dist/chunks/{config-Dpk-eVkS.js → config-DUbbGcfw.js} +12 -1
- package/dist/chunks/config-DUbbGcfw.js.map +1 -0
- package/dist/chunks/{grIconStyles-B6BRqUld.js → grIconStyles-BwBrJY1E.js} +8 -5
- package/dist/chunks/grIconStyles-BwBrJY1E.js.map +1 -0
- package/dist/chunks/grSelectStyles-Cdtd10eY.js.map +1 -1
- package/dist/chunks/{granular-provider-Dds_519x.js → granular-provider-CHFaw3wU.js} +2 -2
- package/dist/chunks/{granular-provider-Dds_519x.js.map → granular-provider-CHFaw3wU.js.map} +1 -1
- package/dist/chunks/renamedProp-BwcN-SJg.js +33 -0
- package/dist/chunks/renamedProp-BwcN-SJg.js.map +1 -0
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-BwhUTqmG.js → GrAutocomplete-X6RlmiJQ.js} +3 -3
- package/dist/components/GrAutocomplete/chunks/GrAutocomplete-X6RlmiJQ.js.map +1 -0
- package/dist/components/GrAutocomplete/index.js +1 -1
- package/dist/components/GrAutocomplete/styles.css +1 -1
- package/dist/components/GrBadge/chunks/{GrBadge-BM3f9PME.js → GrBadge-BsoocpF-.js} +4 -2
- package/dist/components/GrBadge/chunks/GrBadge-BsoocpF-.js.map +1 -0
- package/dist/components/GrBadge/index.js +1 -1
- package/dist/components/GrBadge/styles.css +1 -1
- package/dist/components/GrBreadcrumbs/chunks/{GrBreadcrumbs-BKOvl8W1.js → GrBreadcrumbs-DRDNan_n.js} +1 -1
- package/dist/components/GrBreadcrumbs/chunks/{GrBreadcrumbs-BKOvl8W1.js.map → GrBreadcrumbs-DRDNan_n.js.map} +1 -1
- package/dist/components/GrBreadcrumbs/index.js +1 -1
- package/dist/components/GrCard/chunks/{GrCard-DR3ehBt8.js → GrCard-BttXz1pQ.js} +19 -6
- package/dist/components/GrCard/chunks/GrCard-BttXz1pQ.js.map +1 -0
- package/dist/components/GrCard/index.js +2 -2
- package/dist/components/GrChip/chunks/{GrChip-Df4orxRH.js → GrChip-C0vU69tf.js} +3 -1
- package/dist/components/GrChip/chunks/GrChip-C0vU69tf.js.map +1 -0
- package/dist/components/GrChip/index.js +1 -1
- package/dist/components/GrCollapse/chunks/{GrCollapse-CytiA2Sx.js → GrCollapse-BlW6ybdL.js} +2 -2
- package/dist/components/GrCollapse/chunks/{GrCollapse-CytiA2Sx.js.map → GrCollapse-BlW6ybdL.js.map} +1 -1
- package/dist/components/GrCollapse/index.js +1 -1
- package/dist/components/GrContextMenu/chunks/{GrContextMenu-BNQDbAsY.js → GrContextMenu-_FKwQvuQ.js} +1 -1
- package/dist/components/GrContextMenu/chunks/{GrContextMenu-BNQDbAsY.js.map → GrContextMenu-_FKwQvuQ.js.map} +1 -1
- package/dist/components/GrContextMenu/index.js +1 -1
- package/dist/components/GrDataTable/chunks/{GrDataTable-DU00V44y.js → GrDataTable-C8a9_nCg.js} +1 -1
- package/dist/components/GrDataTable/chunks/{GrDataTable-DU00V44y.js.map → GrDataTable-C8a9_nCg.js.map} +1 -1
- package/dist/components/GrDataTable/index.js +1 -1
- package/dist/components/GrDrawer/chunks/{GrDrawer-BCreL-eq.js → GrDrawer-B7PiAcbU.js} +1 -1
- package/dist/components/GrDrawer/chunks/{GrDrawer-BCreL-eq.js.map → GrDrawer-B7PiAcbU.js.map} +1 -1
- package/dist/components/GrDrawer/index.js +1 -1
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-Cg7qHV9J.js → GrDropdownMenu-GOzuAXsi.js} +1 -1
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-Cg7qHV9J.js.map → GrDropdownMenu-GOzuAXsi.js.map} +1 -1
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenuList-RW5IBvmu.js → GrDropdownMenuList-B33ZzwfB.js} +13 -3
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenuList-RW5IBvmu.js.map → GrDropdownMenuList-B33ZzwfB.js.map} +1 -1
- package/dist/components/GrDropdownMenu/index.js +2 -2
- package/dist/components/GrEmptyState/chunks/{GrEmptyState-D4T5pzXA.js → GrEmptyState-RqsWvgI9.js} +1 -1
- package/dist/components/GrEmptyState/chunks/{GrEmptyState-D4T5pzXA.js.map → GrEmptyState-RqsWvgI9.js.map} +1 -1
- package/dist/components/GrEmptyState/index.js +1 -1
- package/dist/components/GrFileUpload/chunks/{GrFileUpload-FjbaYGjL.js → GrFileUpload-NsHkRRIa.js} +1 -1
- package/dist/components/GrFileUpload/chunks/{GrFileUpload-FjbaYGjL.js.map → GrFileUpload-NsHkRRIa.js.map} +1 -1
- package/dist/components/GrFileUpload/index.js +1 -1
- package/dist/components/GrFormFile/chunks/{GrFormFile-ChdKlN83.js → GrFormFile-CYtXU9HV.js} +1 -1
- package/dist/components/GrFormFile/chunks/{GrFormFile-ChdKlN83.js.map → GrFormFile-CYtXU9HV.js.map} +1 -1
- package/dist/components/GrFormFile/index.js +1 -1
- package/dist/components/GrIcon/chunks/{GrIcon-Bn1XEriL.js → GrIcon-Dmf0c9ba.js} +1 -1
- package/dist/components/GrIcon/chunks/{GrIcon-Bn1XEriL.js.map → GrIcon-Dmf0c9ba.js.map} +1 -1
- package/dist/components/GrIcon/index.js +2 -2
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-COUcpOgR.js → GrImageViewer-CyVN6Zpc.js} +1 -1
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-COUcpOgR.js.map → GrImageViewer-CyVN6Zpc.js.map} +1 -1
- package/dist/components/GrImageViewer/index.js +1 -1
- package/dist/components/GrInputTag/chunks/{GrInputTag-LP3smqOv.js → GrInputTag-Cj4A9Xgs.js} +2 -2
- package/dist/components/GrInputTag/chunks/{GrInputTag-LP3smqOv.js.map → GrInputTag-Cj4A9Xgs.js.map} +1 -1
- package/dist/components/GrInputTag/index.js +1 -1
- package/dist/components/GrLink/chunks/{GrLink-DAGCFYO7.js → GrLink-DYXw3W9w.js} +1 -1
- package/dist/components/GrLink/chunks/{GrLink-DAGCFYO7.js.map → GrLink-DYXw3W9w.js.map} +1 -1
- package/dist/components/GrLink/index.js +1 -1
- package/dist/components/GrList/chunks/{GrList-DrXrKU37.js → GrList-DmT4qzLO.js} +1 -1
- package/dist/components/GrList/chunks/{GrList-DrXrKU37.js.map → GrList-DmT4qzLO.js.map} +1 -1
- package/dist/components/GrList/index.js +1 -1
- package/dist/components/GrLoading/chunks/{GrLoading-DDD8WAIH.js → GrLoading-CMD3Ri9P.js} +1 -1
- package/dist/components/GrLoading/chunks/{GrLoading-DDD8WAIH.js.map → GrLoading-CMD3Ri9P.js.map} +1 -1
- package/dist/components/GrLoading/index.js +1 -1
- package/dist/components/GrNavbar/chunks/{GrNavbar-DhswlYyh.js → GrNavbar-C5EjMKaC.js} +1 -1
- package/dist/components/GrNavbar/chunks/{GrNavbar-DhswlYyh.js.map → GrNavbar-C5EjMKaC.js.map} +1 -1
- package/dist/components/GrNavbar/index.js +1 -1
- package/dist/components/GrNumberInput/chunks/{GrNumberInput-Daud9Jdm.js → GrNumberInput-DQ59YICu.js} +1 -1
- package/dist/components/GrNumberInput/chunks/{GrNumberInput-Daud9Jdm.js.map → GrNumberInput-DQ59YICu.js.map} +1 -1
- package/dist/components/GrNumberInput/index.js +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-kQhWlfs-.js → GrPagination-kYCx6PAS.js} +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-kQhWlfs-.js.map → GrPagination-kYCx6PAS.js.map} +1 -1
- package/dist/components/GrPagination/index.js +1 -1
- package/dist/components/GrSelect/chunks/{GrSelect-D-KU9Lx_.js → GrSelect-CBp0twbF.js} +6 -6
- package/dist/components/GrSelect/chunks/GrSelect-CBp0twbF.js.map +1 -0
- package/dist/components/GrSelect/index.js +1 -1
- package/dist/components/GrSelect/styles.css +1 -1
- package/dist/components/GrSidebar/chunks/{GrSidebar-DxPhIiWs.js → GrSidebar-B8EQIlfW.js} +1 -1
- package/dist/components/GrSidebar/chunks/{GrSidebar-DxPhIiWs.js.map → GrSidebar-B8EQIlfW.js.map} +1 -1
- package/dist/components/GrSidebar/index.js +1 -1
- package/dist/components/GrSortableList/chunks/{GrSortableList-DGs3Pi2J.js → GrSortableList-HyKX--rO.js} +1 -1
- package/dist/components/GrSortableList/chunks/{GrSortableList-DGs3Pi2J.js.map → GrSortableList-HyKX--rO.js.map} +1 -1
- package/dist/components/GrSortableList/index.js +1 -1
- package/dist/components/GrToaster/chunks/{GrToaster-CUi3ldJ0.js → GrToaster-Dp7-4B8k.js} +1 -1
- package/dist/components/GrToaster/chunks/{GrToaster-CUi3ldJ0.js.map → GrToaster-Dp7-4B8k.js.map} +1 -1
- package/dist/components/GrToaster/index.js +1 -1
- package/dist/components/GrTooltip/chunks/{GrTooltip-BKFjqu0E.js → GrTooltip-uYETdbCD.js} +1 -1
- package/dist/components/GrTooltip/chunks/{GrTooltip-BKFjqu0E.js.map → GrTooltip-uYETdbCD.js.map} +1 -1
- package/dist/components/GrTooltip/index.js +1 -1
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-B1GdEn5K.js → GrTreeSelect-CymO68Cv.js} +1 -1
- package/dist/components/GrTreeSelect/chunks/GrTreeSelect-CymO68Cv.js.map +1 -0
- package/dist/components/GrTreeSelect/index.js +1 -1
- package/dist/directives/loading.js +1 -1
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/index.js +31 -31
- package/dist/types/components/GrCard/GrCard.vue.d.ts +8 -0
- package/dist/types/components/GrCard/grCardStyles.d.ts +9 -0
- package/dist/types/components/GrDropdownMenu/menuModel.d.ts +20 -2
- package/dist/types/components/GrIcon/grIconStyles.d.ts +7 -4
- package/dist/types/components/GrSelect/grSelectStyles.d.ts +12 -0
- package/dist/types/components/shared/renamedProp.d.ts +19 -0
- package/dist/web-types.json +5 -1
- package/package.json +2 -2
- package/dist/chunks/config-Dpk-eVkS.js.map +0 -1
- package/dist/chunks/grIconStyles-B6BRqUld.js.map +0 -1
- package/dist/components/GrAutocomplete/chunks/GrAutocomplete-BwhUTqmG.js.map +0 -1
- package/dist/components/GrBadge/chunks/GrBadge-BM3f9PME.js.map +0 -1
- package/dist/components/GrCard/chunks/GrCard-DR3ehBt8.js.map +0 -1
- package/dist/components/GrChip/chunks/GrChip-Df4orxRH.js.map +0 -1
- package/dist/components/GrSelect/chunks/GrSelect-D-KU9Lx_.js.map +0 -1
- package/dist/components/GrTreeSelect/chunks/GrTreeSelect-B1GdEn5K.js.map +0 -1
package/dist/components/GrFormFile/chunks/{GrFormFile-ChdKlN83.js.map → GrFormFile-CYtXU9HV.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrFormFile-ChdKlN83.js","names":[],"sources":["../../../../src/components/GrFormFile/GrFormFile.vue","../../../../src/components/GrFormFile/GrFormFile.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, ref, useId, watch } from 'vue'\n\nimport IconUpload from '~icons/lucide/upload'\nimport IconX from '~icons/lucide/x'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n type GrFormFileSize,\n buttonSizes,\n iconOffsets,\n iconSizes,\n previewBaseClass,\n previewSizes,\n removeTextSizes,\n rowGaps,\n stackGaps,\n textSizes,\n} from './grFormFileStyles'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useFilePreviews } from '../../composables/internal/useFilePreviews'\nimport { vDropzone } from '../../directives'\nimport { acceptValidator, FileValidationError, maxCountValidator, resolveFileValidationMessage, runFileValidators } from '../../fileValidation'\nimport type { FileValidationIssue, FileValidator } from '../../fileValidation'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nexport type GrFormFileError = FileValidationIssue\n\n/**\n * Пропы `GrFormFile` — форма-поле для выбора файлов с встроенной валидацией,\n * drag&drop (через `v-dropzone`) и списком выбранных файлов.\n *\n * Все `*Text`/`placeholder` — i18n-friendly, принимают готовые строки локали.\n */\nexport interface GrFormFileProps {\n modelValue: File | File[] | null\n multiple?: 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 /** W3C `accept` для `<input type=\"file\">` + sugar к `acceptValidator(...)`. */\n accept?: string\n validators?: FileValidator[]\n /** Максимум файлов в наборе. Лишние не обрезаются молча — набор отбивается ошибкой. */\n limit?: number\n uploadText?: string\n changeText?: string\n removeText?: string\n clearAllText?: string\n placeholder?: string\n /** Размер кнопок, иконок и подписей. */\n size?: GrFormFileSize\n /** Миниатюры для картинок в наборе. Файлы других типов остаются строкой. */\n preview?: boolean\n /** Дополнительная (кастомная) валидация на стороне потребителя. */\n validate?: (files: File[]) => GrFormFileError[] | Promise<GrFormFileError[]>\n /**\n * Контролируемый список ошибок: `v-model:errors`. Задан — показывается он, и\n * внутренняя валидация его не перетирает. Сюда же кладутся ошибки, пришедшие\n * с сервера. Не задан — компонент держит свои ошибки сам.\n */\n errors?: GrFormFileError[]\n}\n\nexport interface GrFormFileEmits {\n (e: 'update:modelValue', value: File | File[] | null): void\n (e: 'change', value: File | File[] | null): void\n (e: 'clear'): void\n /** Результат валидации. Канал один: `validation` дублировал эту же нагрузку. */\n (e: 'update:errors', errors: GrFormFileError[]): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormFileProps>(),\n {\n multiple: false,\n preview: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n accept: undefined,\n limit: undefined,\n uploadText: undefined,\n changeText: undefined,\n removeText: undefined,\n clearAllText: undefined,\n placeholder: undefined,\n size: undefined,\n validators: undefined,\n validate: undefined,\n errors: undefined,\n },\n)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFormFile' })\n\nconst rowClass = computed(() => rowGaps[resolvedSize.value])\nconst stackClass = computed(() => stackGaps[resolvedSize.value])\nconst textClass = computed(() => textSizes[resolvedSize.value])\nconst removeTextClass = computed(() => removeTextSizes[resolvedSize.value])\nconst iconOffsetClass = computed(() => iconOffsets[resolvedSize.value])\nconst buttonSize = computed(() => buttonSizes[resolvedSize.value])\nconst iconSize = computed(() => iconSizes[resolvedSize.value])\nconst previewClass = computed(() => [previewSizes[resolvedSize.value], previewBaseClass])\n\nconst emit = defineEmits<GrFormFileEmits>()\ndefineSlots<{\n /** Собственный вывод ошибок вместо списка по умолчанию. */\n error?: (props: { errors: GrFormFileError[] }) => any\n}>()\n\n\nconst { t } = useGranularityTranslations()\n\n// Контекст `GrFormField`. Виджет здесь — кнопка выбора файла, а не нативный\n// `<input type=\"file\">`: он `aria-hidden` и вне таб-порядка, id на нём увёл бы\n// `<label for>` в невидимый элемент.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n // `locked` — «ввод не принимается»: `disabled` или `readonly`. Набор в\n // `readonly` виден и уходит в форму, но поменять его нельзя ни диалогом, ни\n // перетаскиванием, ни удалением.\n locked: isLocked,\n} = useGrFormControl(() => props)\nconst resolvedUploadText = computed(() => props.uploadText ?? t('gr.formFile.upload', 'Upload file'))\nconst resolvedChangeText = computed(() => props.changeText ?? t('gr.formFile.change', 'Change file'))\nconst resolvedRemoveText = computed(() => props.removeText ?? t('gr.formFile.remove', 'Remove'))\nconst resolvedClearAllText = computed(() => props.clearAllText ?? t('gr.formFile.clearAll', 'Clear all'))\nconst resolvedPlaceholder = computed(() => props.placeholder ?? t('gr.formFile.placeholder', 'No files selected'))\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})\nconst localErrors = ref<GrFormFileError[]>([])\nconst uploadBtnEl = ref<HTMLElement | null>(null)\n\n// Собственные ошибки компонент обязан и объявить (`role=\"alert\"`), и связать с\n// контролом: до этого «уронил файл не того типа» для скринридера выглядело как\n// «ничего не произошло». Ошибка поля из `GrFormField` при этом остаётся —\n// `aria-describedby` держит оба id.\nconst errorsId = useId()\n// Контролируемый список сильнее внутреннего — как `sortKey` у `GrDataTable`.\nconst displayedErrors = computed(() => props.errors ?? localErrors.value)\nconst hasLocalErrors = computed(() => displayedErrors.value.length > 0)\n\n/**\n * `aria-required` и `aria-readonly` роль `button` не поддерживает — axe роняет\n * это как critical `aria-allowed-attr`. Состояния уходят в описание кнопки:\n * иначе они пропали бы для диктора совсем (маркер `*` у `GrFormField`\n * декоративен и скрыт).\n */\nconst stateHintId = useId()\nconst stateHint = computed(() => [\n isRequired.value ? t('gr.form.required', 'This field is required') : undefined,\n isReadonly.value ? t('gr.form.readonly', 'Read only') : undefined,\n].filter(Boolean).join('. ') || undefined)\n\nconst describedByIds = computed(() => {\n return [\n field?.describedById.value,\n hasLocalErrors.value ? errorsId : undefined,\n stateHint.value ? stateHintId : undefined,\n ]\n .filter(Boolean)\n .join(' ') || undefined\n})\n\nconst showsInvalid = computed(() => isInvalid.value || hasLocalErrors.value)\n\nfunction focus(): void {\n uploadBtnEl.value?.focus()\n}\n\nfunction blur(): void {\n uploadBtnEl.value?.blur()\n}\n\ndefineExpose({ focus, blur })\n\nconst files = computed<File[]>(() => {\n if (props.multiple) {\n return Array.isArray(props.modelValue) ? props.modelValue : []\n }\n\n return props.modelValue instanceof File ? [props.modelValue] : []\n})\n\nconst hasFiles = computed(() => files.value.length > 0)\n\nconst { fileKey, previewUrl, revokePreview, revokeAllPreviews } = useFilePreviews({\n enabled: () => props.preview,\n})\n\n/**\n * Отзыв `object URL` привязан к самому набору, а не к местам, где его меняют.\n * Файл уходит из набора десятком путей — кнопка строки, «очистить всё», новый\n * выбор, внешний сброс `v-model`, — и точечные вызовы разъехались бы с первой\n * же новой веткой; blob при этом висел бы в памяти вкладки до перезагрузки.\n */\nwatch(files, (next, prev) => {\n for (const file of prev ?? []) {\n if (!next.includes(file)) revokePreview(file)\n }\n})\n\nonBeforeUnmount(revokeAllPreviews)\n\n/**\n * Один набор валидаторов на оба пути ввода. Собери его отдельно в `applyFiles`\n * и в `dropzone` — копии разъедутся при первой же правке, и выбор через диалог\n * начнёт вести себя иначе, чем перетаскивание.\n */\nconst effectiveValidators = computed<FileValidator[]>(() => {\n const customValidator: FileValidator | undefined = props.validate\n ? async ({ files }) => (await props.validate?.(files)) ?? []\n : undefined\n\n return [\n acceptValidator(props.accept),\n maxCountValidator(props.limit),\n ...(props.validators ?? []),\n ...(customValidator ? [customValidator] : []),\n ]\n})\n\nfunction clearInputValue() {\n if (!inputRef.value) return\n inputRef.value.value = ''\n}\n\nfunction setErrors(next: GrFormFileError[]) {\n localErrors.value = next\n emit('update:errors', next)\n}\n\nfunction clearErrors() {\n if (displayedErrors.value.length === 0) return\n setErrors([])\n}\n\nfunction openDialog() {\n if (isLocked.value) return\n inputRef.value?.click()\n}\n\nfunction emitModel(nextFiles: File[]) {\n const value: File | File[] | null = props.multiple\n ? nextFiles\n : (nextFiles[0] ?? null)\n\n emit('update:modelValue', value)\n emit('change', value)\n}\n\nasync function applyFiles(nextFiles: File[]) {\n const isPromiseLike = (value: unknown): value is PromiseLike<any> => {\n return !!value && typeof (value as any).then === 'function'\n }\n\n const res = runFileValidators(nextFiles, effectiveValidators.value, {\n source: 'input',\n multiple: props.multiple,\n })\n\n const { files: picked, issues } = isPromiseLike(res) ? await res : res\n\n if (issues.length > 0) {\n setErrors(issues)\n clearInputValue()\n await nextTick()\n return\n }\n\n clearErrors()\n emitModel(picked)\n clearInputValue()\n await nextTick()\n}\n\nasync function onInputChange(event: Event) {\n if (isLocked.value) return\n\n const target = event.target as HTMLInputElement | null\n const nextFiles = target?.files ? Array.prototype.slice.call(target.files) as File[] : []\n\n // Reset value so selecting the same file twice triggers change.\n if (target) target.value = ''\n await nextTick()\n\n await applyFiles(nextFiles)\n}\n\nfunction clearAll() {\n if (isLocked.value) return\n clearErrors()\n clearInputValue()\n emit('clear')\n\n emit('update:modelValue', props.multiple ? [] : null)\n emit('change', props.multiple ? [] : null)\n}\n\nfunction removeAt(index: number) {\n if (isLocked.value) return\n if (!props.multiple) {\n clearAll()\n return\n }\n\n const next = files.value.slice()\n next.splice(index, 1)\n\n emit('update:modelValue', next)\n emit('change', next)\n}\n\n/** Размер файла в строке списка — как в `GrFileUpload`: без него список не отвечает «сколько». */\nfunction formatFileSize(file: File): string {\n return `${Math.ceil(file.size / 1024)} KB`\n}\n\nfunction issueMessage(issue: GrFormFileError): string {\n const text = resolveFileValidationMessage(issue, t)\n\n // Префикс с именем файла нужен только тем сообщениям, которые сами его не\n // называют, — то есть валидаторам потребителя. Встроенные передают `fileName`\n // параметром и подставляют его в текст, и приписка давала бы «photo.png:\n // Файл «photo.png» …». Признак — наличие параметра, а не поиск подстроки.\n if (issue.fileName && issue.i18nParams?.fileName === undefined)\n return `${issue.fileName}: ${text}`\n\n return text\n}\n\nconst dropzone = computed(() => {\n return {\n enabled: !isLocked.value,\n multiple: props.multiple,\n validators: effectiveValidators.value,\n onFiles: async (dropped: File[]) => {\n // `v-dropzone` уже выполнил валидаторы и нормализацию по `multiple`.\n clearErrors()\n emitModel(dropped)\n clearInputValue()\n await nextTick()\n },\n onError: (error: unknown) => {\n if (error instanceof FileValidationError) {\n setErrors(error.issues)\n }\n else {\n setErrors([\n {\n code: 'accept',\n message: t('gr.formFile.validateError', 'Failed to validate dropped files'),\n },\n ])\n }\n },\n }\n})\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (!inputRef.value) return\n\n if (!value || (Array.isArray(value) && value.length === 0)) {\n // Позволяет выбрать тот же файл снова после внешнего сброса.\n inputRef.value.value = ''\n }\n },\n)\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n v-dropzone=\"dropzone\"\n data-gr-form-file\n class=\"rounded-[var(--gr-radius-md)]\"\n :class=\"isDisabled ? 'cursor-not-allowed' : ''\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <span v-if=\"stateHint\" :id=\"stateHintId\" class=\"sr-only\">{{ stateHint }}</span>\n\n <input\n ref=\"inputRef\"\n data-gr-form-file-input\n type=\"file\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n class=\"absolute opacity-0 w-px h-px pointer-events-none\"\n :multiple=\"multiple\"\n :accept=\"accept\"\n :disabled=\"isDisabled\"\n @change=\"onInputChange\"\n >\n\n <div class=\"flex flex-col\" :class=\"stackClass\">\n <div class=\"flex flex-wrap items-center\" :class=\"rowClass\">\n <GrButton\n :id=\"fieldId\"\n ref=\"uploadBtnEl\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-upload-btn\n :aria-describedby=\"describedByIds\"\n :aria-invalid=\"showsInvalid ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :disabled=\"isDisabled\"\n @click.prevent=\"openDialog\"\n >\n <GrIcon :size=\"iconSize\">\n <IconUpload />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ hasFiles ? resolvedChangeText : resolvedUploadText }}</span>\n </GrButton>\n\n <GrButton\n v-if=\"hasFiles && !multiple && !isReadonly\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-clear-btn\n :disabled=\"isDisabled\"\n @click.prevent=\"clearAll\"\n >\n <GrIcon :size=\"iconSize\">\n <IconX />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ resolvedRemoveText }}</span>\n </GrButton>\n\n <GrButton\n v-if=\"multiple && hasFiles && !isReadonly\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-clear-all-btn\n :disabled=\"isDisabled\"\n @click.prevent=\"clearAll\"\n >\n <GrIcon :size=\"iconSize\">\n <IconX />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ resolvedClearAllText }}</span>\n </GrButton>\n\n <img\n v-if=\"!multiple && hasFiles && previewUrl(files[0]!)\"\n data-gr-form-file-preview\n :src=\"previewUrl(files[0]!)\"\n alt=\"\"\n :class=\"previewClass\"\n >\n\n <span\n v-if=\"!multiple && hasFiles\"\n class=\"text-[var(--gr-muted-fg)] truncate max-w-[240px]\"\n :class=\"textClass\"\n data-gr-form-file-single-name\n :title=\"files[0]?.name\"\n >\n {{ files[0]?.name }}\n </span>\n\n <span\n v-if=\"!hasFiles\"\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"textClass\"\n data-gr-form-file-placeholder\n >\n {{ resolvedPlaceholder }}\n </span>\n </div>\n\n <div v-if=\"multiple && hasFiles\" class=\"flex flex-col\" :class=\"stackClass\">\n <div\n v-for=\"(file, index) in files\"\n :key=\"fileKey(file)\"\n class=\"flex items-center gap-2\"\n data-gr-form-file-item\n >\n <img\n v-if=\"previewUrl(file)\"\n data-gr-form-file-preview\n :src=\"previewUrl(file)\"\n alt=\"\"\n :class=\"previewClass\"\n >\n\n <span\n class=\"text-[var(--gr-muted-fg)] truncate max-w-[240px]\"\n :class=\"textClass\"\n :title=\"file.name\"\n data-gr-form-file-item-name\n >\n {{ file.name }}\n </span>\n\n <span\n class=\"text-[var(--gr-muted-fg)] shrink-0\"\n :class=\"removeTextClass\"\n data-gr-form-file-item-size\n >{{ formatFileSize(file) }}</span>\n\n <button\n v-if=\"!isReadonly\"\n type=\"button\"\n class=\"text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)]\"\n :class=\"removeTextClass\"\n data-gr-form-file-item-remove\n :disabled=\"isDisabled\"\n :aria-label=\"t('gr.formFile.removeFile', 'Remove {fileName}', { fileName: file.name })\"\n @click.prevent=\"removeAt(index)\"\n >\n {{ resolvedRemoveText }}\n </button>\n </div>\n </div>\n\n <slot name=\"error\" :errors=\"displayedErrors\">\n <div\n v-if=\"hasLocalErrors\"\n :id=\"errorsId\"\n class=\"text-[var(--gr-danger-text)]\"\n :class=\"textClass\"\n data-gr-form-file-errors\n role=\"alert\"\n >\n <div v-for=\"(e, i) in displayedErrors\" :key=\"i\" data-gr-form-file-error>\n {{ issueMessage(e) }}\n </div>\n </div>\n </slot>\n </div>\n </div>\n</template>\n\n<style scoped>\n[data-gr-form-file].gr-dropzone--over {\n outline: 2px solid var(--gr-ring);\n outline-offset: 2px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, ref, useId, watch } from 'vue'\n\nimport IconUpload from '~icons/lucide/upload'\nimport IconX from '~icons/lucide/x'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n type GrFormFileSize,\n buttonSizes,\n iconOffsets,\n iconSizes,\n previewBaseClass,\n previewSizes,\n removeTextSizes,\n rowGaps,\n stackGaps,\n textSizes,\n} from './grFormFileStyles'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useFilePreviews } from '../../composables/internal/useFilePreviews'\nimport { vDropzone } from '../../directives'\nimport { acceptValidator, FileValidationError, maxCountValidator, resolveFileValidationMessage, runFileValidators } from '../../fileValidation'\nimport type { FileValidationIssue, FileValidator } from '../../fileValidation'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nexport type GrFormFileError = FileValidationIssue\n\n/**\n * Пропы `GrFormFile` — форма-поле для выбора файлов с встроенной валидацией,\n * drag&drop (через `v-dropzone`) и списком выбранных файлов.\n *\n * Все `*Text`/`placeholder` — i18n-friendly, принимают готовые строки локали.\n */\nexport interface GrFormFileProps {\n modelValue: File | File[] | null\n multiple?: 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 /** W3C `accept` для `<input type=\"file\">` + sugar к `acceptValidator(...)`. */\n accept?: string\n validators?: FileValidator[]\n /** Максимум файлов в наборе. Лишние не обрезаются молча — набор отбивается ошибкой. */\n limit?: number\n uploadText?: string\n changeText?: string\n removeText?: string\n clearAllText?: string\n placeholder?: string\n /** Размер кнопок, иконок и подписей. */\n size?: GrFormFileSize\n /** Миниатюры для картинок в наборе. Файлы других типов остаются строкой. */\n preview?: boolean\n /** Дополнительная (кастомная) валидация на стороне потребителя. */\n validate?: (files: File[]) => GrFormFileError[] | Promise<GrFormFileError[]>\n /**\n * Контролируемый список ошибок: `v-model:errors`. Задан — показывается он, и\n * внутренняя валидация его не перетирает. Сюда же кладутся ошибки, пришедшие\n * с сервера. Не задан — компонент держит свои ошибки сам.\n */\n errors?: GrFormFileError[]\n}\n\nexport interface GrFormFileEmits {\n (e: 'update:modelValue', value: File | File[] | null): void\n (e: 'change', value: File | File[] | null): void\n (e: 'clear'): void\n /** Результат валидации. Канал один: `validation` дублировал эту же нагрузку. */\n (e: 'update:errors', errors: GrFormFileError[]): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormFileProps>(),\n {\n multiple: false,\n preview: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n accept: undefined,\n limit: undefined,\n uploadText: undefined,\n changeText: undefined,\n removeText: undefined,\n clearAllText: undefined,\n placeholder: undefined,\n size: undefined,\n validators: undefined,\n validate: undefined,\n errors: undefined,\n },\n)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFormFile' })\n\nconst rowClass = computed(() => rowGaps[resolvedSize.value])\nconst stackClass = computed(() => stackGaps[resolvedSize.value])\nconst textClass = computed(() => textSizes[resolvedSize.value])\nconst removeTextClass = computed(() => removeTextSizes[resolvedSize.value])\nconst iconOffsetClass = computed(() => iconOffsets[resolvedSize.value])\nconst buttonSize = computed(() => buttonSizes[resolvedSize.value])\nconst iconSize = computed(() => iconSizes[resolvedSize.value])\nconst previewClass = computed(() => [previewSizes[resolvedSize.value], previewBaseClass])\n\nconst emit = defineEmits<GrFormFileEmits>()\ndefineSlots<{\n /** Собственный вывод ошибок вместо списка по умолчанию. */\n error?: (props: { errors: GrFormFileError[] }) => any\n}>()\n\n\nconst { t } = useGranularityTranslations()\n\n// Контекст `GrFormField`. Виджет здесь — кнопка выбора файла, а не нативный\n// `<input type=\"file\">`: он `aria-hidden` и вне таб-порядка, id на нём увёл бы\n// `<label for>` в невидимый элемент.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n // `locked` — «ввод не принимается»: `disabled` или `readonly`. Набор в\n // `readonly` виден и уходит в форму, но поменять его нельзя ни диалогом, ни\n // перетаскиванием, ни удалением.\n locked: isLocked,\n} = useGrFormControl(() => props)\nconst resolvedUploadText = computed(() => props.uploadText ?? t('gr.formFile.upload', 'Upload file'))\nconst resolvedChangeText = computed(() => props.changeText ?? t('gr.formFile.change', 'Change file'))\nconst resolvedRemoveText = computed(() => props.removeText ?? t('gr.formFile.remove', 'Remove'))\nconst resolvedClearAllText = computed(() => props.clearAllText ?? t('gr.formFile.clearAll', 'Clear all'))\nconst resolvedPlaceholder = computed(() => props.placeholder ?? t('gr.formFile.placeholder', 'No files selected'))\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})\nconst localErrors = ref<GrFormFileError[]>([])\nconst uploadBtnEl = ref<HTMLElement | null>(null)\n\n// Собственные ошибки компонент обязан и объявить (`role=\"alert\"`), и связать с\n// контролом: до этого «уронил файл не того типа» для скринридера выглядело как\n// «ничего не произошло». Ошибка поля из `GrFormField` при этом остаётся —\n// `aria-describedby` держит оба id.\nconst errorsId = useId()\n// Контролируемый список сильнее внутреннего — как `sortKey` у `GrDataTable`.\nconst displayedErrors = computed(() => props.errors ?? localErrors.value)\nconst hasLocalErrors = computed(() => displayedErrors.value.length > 0)\n\n/**\n * `aria-required` и `aria-readonly` роль `button` не поддерживает — axe роняет\n * это как critical `aria-allowed-attr`. Состояния уходят в описание кнопки:\n * иначе они пропали бы для диктора совсем (маркер `*` у `GrFormField`\n * декоративен и скрыт).\n */\nconst stateHintId = useId()\nconst stateHint = computed(() => [\n isRequired.value ? t('gr.form.required', 'This field is required') : undefined,\n isReadonly.value ? t('gr.form.readonly', 'Read only') : undefined,\n].filter(Boolean).join('. ') || undefined)\n\nconst describedByIds = computed(() => {\n return [\n field?.describedById.value,\n hasLocalErrors.value ? errorsId : undefined,\n stateHint.value ? stateHintId : undefined,\n ]\n .filter(Boolean)\n .join(' ') || undefined\n})\n\nconst showsInvalid = computed(() => isInvalid.value || hasLocalErrors.value)\n\nfunction focus(): void {\n uploadBtnEl.value?.focus()\n}\n\nfunction blur(): void {\n uploadBtnEl.value?.blur()\n}\n\ndefineExpose({ focus, blur })\n\nconst files = computed<File[]>(() => {\n if (props.multiple) {\n return Array.isArray(props.modelValue) ? props.modelValue : []\n }\n\n return props.modelValue instanceof File ? [props.modelValue] : []\n})\n\nconst hasFiles = computed(() => files.value.length > 0)\n\nconst { fileKey, previewUrl, revokePreview, revokeAllPreviews } = useFilePreviews({\n enabled: () => props.preview,\n})\n\n/**\n * Отзыв `object URL` привязан к самому набору, а не к местам, где его меняют.\n * Файл уходит из набора десятком путей — кнопка строки, «очистить всё», новый\n * выбор, внешний сброс `v-model`, — и точечные вызовы разъехались бы с первой\n * же новой веткой; blob при этом висел бы в памяти вкладки до перезагрузки.\n */\nwatch(files, (next, prev) => {\n for (const file of prev ?? []) {\n if (!next.includes(file)) revokePreview(file)\n }\n})\n\nonBeforeUnmount(revokeAllPreviews)\n\n/**\n * Один набор валидаторов на оба пути ввода. Собери его отдельно в `applyFiles`\n * и в `dropzone` — копии разъедутся при первой же правке, и выбор через диалог\n * начнёт вести себя иначе, чем перетаскивание.\n */\nconst effectiveValidators = computed<FileValidator[]>(() => {\n const customValidator: FileValidator | undefined = props.validate\n ? async ({ files }) => (await props.validate?.(files)) ?? []\n : undefined\n\n return [\n acceptValidator(props.accept),\n maxCountValidator(props.limit),\n ...(props.validators ?? []),\n ...(customValidator ? [customValidator] : []),\n ]\n})\n\nfunction clearInputValue() {\n if (!inputRef.value) return\n inputRef.value.value = ''\n}\n\nfunction setErrors(next: GrFormFileError[]) {\n localErrors.value = next\n emit('update:errors', next)\n}\n\nfunction clearErrors() {\n if (displayedErrors.value.length === 0) return\n setErrors([])\n}\n\nfunction openDialog() {\n if (isLocked.value) return\n inputRef.value?.click()\n}\n\nfunction emitModel(nextFiles: File[]) {\n const value: File | File[] | null = props.multiple\n ? nextFiles\n : (nextFiles[0] ?? null)\n\n emit('update:modelValue', value)\n emit('change', value)\n}\n\nasync function applyFiles(nextFiles: File[]) {\n const isPromiseLike = (value: unknown): value is PromiseLike<any> => {\n return !!value && typeof (value as any).then === 'function'\n }\n\n const res = runFileValidators(nextFiles, effectiveValidators.value, {\n source: 'input',\n multiple: props.multiple,\n })\n\n const { files: picked, issues } = isPromiseLike(res) ? await res : res\n\n if (issues.length > 0) {\n setErrors(issues)\n clearInputValue()\n await nextTick()\n return\n }\n\n clearErrors()\n emitModel(picked)\n clearInputValue()\n await nextTick()\n}\n\nasync function onInputChange(event: Event) {\n if (isLocked.value) return\n\n const target = event.target as HTMLInputElement | null\n const nextFiles = target?.files ? Array.prototype.slice.call(target.files) as File[] : []\n\n // Reset value so selecting the same file twice triggers change.\n if (target) target.value = ''\n await nextTick()\n\n await applyFiles(nextFiles)\n}\n\nfunction clearAll() {\n if (isLocked.value) return\n clearErrors()\n clearInputValue()\n emit('clear')\n\n emit('update:modelValue', props.multiple ? [] : null)\n emit('change', props.multiple ? [] : null)\n}\n\nfunction removeAt(index: number) {\n if (isLocked.value) return\n if (!props.multiple) {\n clearAll()\n return\n }\n\n const next = files.value.slice()\n next.splice(index, 1)\n\n emit('update:modelValue', next)\n emit('change', next)\n}\n\n/** Размер файла в строке списка — как в `GrFileUpload`: без него список не отвечает «сколько». */\nfunction formatFileSize(file: File): string {\n return `${Math.ceil(file.size / 1024)} KB`\n}\n\nfunction issueMessage(issue: GrFormFileError): string {\n const text = resolveFileValidationMessage(issue, t)\n\n // Префикс с именем файла нужен только тем сообщениям, которые сами его не\n // называют, — то есть валидаторам потребителя. Встроенные передают `fileName`\n // параметром и подставляют его в текст, и приписка давала бы «photo.png:\n // Файл «photo.png» …». Признак — наличие параметра, а не поиск подстроки.\n if (issue.fileName && issue.i18nParams?.fileName === undefined)\n return `${issue.fileName}: ${text}`\n\n return text\n}\n\nconst dropzone = computed(() => {\n return {\n enabled: !isLocked.value,\n multiple: props.multiple,\n validators: effectiveValidators.value,\n onFiles: async (dropped: File[]) => {\n // `v-dropzone` уже выполнил валидаторы и нормализацию по `multiple`.\n clearErrors()\n emitModel(dropped)\n clearInputValue()\n await nextTick()\n },\n onError: (error: unknown) => {\n if (error instanceof FileValidationError) {\n setErrors(error.issues)\n }\n else {\n setErrors([\n {\n code: 'accept',\n message: t('gr.formFile.validateError', 'Failed to validate dropped files'),\n },\n ])\n }\n },\n }\n})\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (!inputRef.value) return\n\n if (!value || (Array.isArray(value) && value.length === 0)) {\n // Позволяет выбрать тот же файл снова после внешнего сброса.\n inputRef.value.value = ''\n }\n },\n)\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n v-dropzone=\"dropzone\"\n data-gr-form-file\n class=\"rounded-[var(--gr-radius-md)]\"\n :class=\"isDisabled ? 'cursor-not-allowed' : ''\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <span v-if=\"stateHint\" :id=\"stateHintId\" class=\"sr-only\">{{ stateHint }}</span>\n\n <input\n ref=\"inputRef\"\n data-gr-form-file-input\n type=\"file\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n class=\"absolute opacity-0 w-px h-px pointer-events-none\"\n :multiple=\"multiple\"\n :accept=\"accept\"\n :disabled=\"isDisabled\"\n @change=\"onInputChange\"\n >\n\n <div class=\"flex flex-col\" :class=\"stackClass\">\n <div class=\"flex flex-wrap items-center\" :class=\"rowClass\">\n <GrButton\n :id=\"fieldId\"\n ref=\"uploadBtnEl\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-upload-btn\n :aria-describedby=\"describedByIds\"\n :aria-invalid=\"showsInvalid ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :disabled=\"isDisabled\"\n @click.prevent=\"openDialog\"\n >\n <GrIcon :size=\"iconSize\">\n <IconUpload />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ hasFiles ? resolvedChangeText : resolvedUploadText }}</span>\n </GrButton>\n\n <GrButton\n v-if=\"hasFiles && !multiple && !isReadonly\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-clear-btn\n :disabled=\"isDisabled\"\n @click.prevent=\"clearAll\"\n >\n <GrIcon :size=\"iconSize\">\n <IconX />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ resolvedRemoveText }}</span>\n </GrButton>\n\n <GrButton\n v-if=\"multiple && hasFiles && !isReadonly\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-clear-all-btn\n :disabled=\"isDisabled\"\n @click.prevent=\"clearAll\"\n >\n <GrIcon :size=\"iconSize\">\n <IconX />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ resolvedClearAllText }}</span>\n </GrButton>\n\n <img\n v-if=\"!multiple && hasFiles && previewUrl(files[0]!)\"\n data-gr-form-file-preview\n :src=\"previewUrl(files[0]!)\"\n alt=\"\"\n :class=\"previewClass\"\n >\n\n <span\n v-if=\"!multiple && hasFiles\"\n class=\"text-[var(--gr-muted-fg)] truncate max-w-[240px]\"\n :class=\"textClass\"\n data-gr-form-file-single-name\n :title=\"files[0]?.name\"\n >\n {{ files[0]?.name }}\n </span>\n\n <span\n v-if=\"!hasFiles\"\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"textClass\"\n data-gr-form-file-placeholder\n >\n {{ resolvedPlaceholder }}\n </span>\n </div>\n\n <div v-if=\"multiple && hasFiles\" class=\"flex flex-col\" :class=\"stackClass\">\n <div\n v-for=\"(file, index) in files\"\n :key=\"fileKey(file)\"\n class=\"flex items-center gap-2\"\n data-gr-form-file-item\n >\n <img\n v-if=\"previewUrl(file)\"\n data-gr-form-file-preview\n :src=\"previewUrl(file)\"\n alt=\"\"\n :class=\"previewClass\"\n >\n\n <span\n class=\"text-[var(--gr-muted-fg)] truncate max-w-[240px]\"\n :class=\"textClass\"\n :title=\"file.name\"\n data-gr-form-file-item-name\n >\n {{ file.name }}\n </span>\n\n <span\n class=\"text-[var(--gr-muted-fg)] shrink-0\"\n :class=\"removeTextClass\"\n data-gr-form-file-item-size\n >{{ formatFileSize(file) }}</span>\n\n <button\n v-if=\"!isReadonly\"\n type=\"button\"\n class=\"text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)]\"\n :class=\"removeTextClass\"\n data-gr-form-file-item-remove\n :disabled=\"isDisabled\"\n :aria-label=\"t('gr.formFile.removeFile', 'Remove {fileName}', { fileName: file.name })\"\n @click.prevent=\"removeAt(index)\"\n >\n {{ resolvedRemoveText }}\n </button>\n </div>\n </div>\n\n <slot name=\"error\" :errors=\"displayedErrors\">\n <div\n v-if=\"hasLocalErrors\"\n :id=\"errorsId\"\n class=\"text-[var(--gr-danger-text)]\"\n :class=\"textClass\"\n data-gr-form-file-errors\n role=\"alert\"\n >\n <div v-for=\"(e, i) in displayedErrors\" :key=\"i\" data-gr-form-file-error>\n {{ issueMessage(e) }}\n </div>\n </div>\n </slot>\n </div>\n </div>\n</template>\n\n<style scoped>\n[data-gr-form-file].gr-dropzone--over {\n outline: 2px solid var(--gr-ring);\n outline-offset: 2px;\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrFormFile-CYtXU9HV.js","names":[],"sources":["../../../../src/components/GrFormFile/GrFormFile.vue","../../../../src/components/GrFormFile/GrFormFile.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, ref, useId, watch } from 'vue'\n\nimport IconUpload from '~icons/lucide/upload'\nimport IconX from '~icons/lucide/x'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n type GrFormFileSize,\n buttonSizes,\n iconOffsets,\n iconSizes,\n previewBaseClass,\n previewSizes,\n removeTextSizes,\n rowGaps,\n stackGaps,\n textSizes,\n} from './grFormFileStyles'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useFilePreviews } from '../../composables/internal/useFilePreviews'\nimport { vDropzone } from '../../directives'\nimport { acceptValidator, FileValidationError, maxCountValidator, resolveFileValidationMessage, runFileValidators } from '../../fileValidation'\nimport type { FileValidationIssue, FileValidator } from '../../fileValidation'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nexport type GrFormFileError = FileValidationIssue\n\n/**\n * Пропы `GrFormFile` — форма-поле для выбора файлов с встроенной валидацией,\n * drag&drop (через `v-dropzone`) и списком выбранных файлов.\n *\n * Все `*Text`/`placeholder` — i18n-friendly, принимают готовые строки локали.\n */\nexport interface GrFormFileProps {\n modelValue: File | File[] | null\n multiple?: 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 /** W3C `accept` для `<input type=\"file\">` + sugar к `acceptValidator(...)`. */\n accept?: string\n validators?: FileValidator[]\n /** Максимум файлов в наборе. Лишние не обрезаются молча — набор отбивается ошибкой. */\n limit?: number\n uploadText?: string\n changeText?: string\n removeText?: string\n clearAllText?: string\n placeholder?: string\n /** Размер кнопок, иконок и подписей. */\n size?: GrFormFileSize\n /** Миниатюры для картинок в наборе. Файлы других типов остаются строкой. */\n preview?: boolean\n /** Дополнительная (кастомная) валидация на стороне потребителя. */\n validate?: (files: File[]) => GrFormFileError[] | Promise<GrFormFileError[]>\n /**\n * Контролируемый список ошибок: `v-model:errors`. Задан — показывается он, и\n * внутренняя валидация его не перетирает. Сюда же кладутся ошибки, пришедшие\n * с сервера. Не задан — компонент держит свои ошибки сам.\n */\n errors?: GrFormFileError[]\n}\n\nexport interface GrFormFileEmits {\n (e: 'update:modelValue', value: File | File[] | null): void\n (e: 'change', value: File | File[] | null): void\n (e: 'clear'): void\n /** Результат валидации. Канал один: `validation` дублировал эту же нагрузку. */\n (e: 'update:errors', errors: GrFormFileError[]): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormFileProps>(),\n {\n multiple: false,\n preview: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n accept: undefined,\n limit: undefined,\n uploadText: undefined,\n changeText: undefined,\n removeText: undefined,\n clearAllText: undefined,\n placeholder: undefined,\n size: undefined,\n validators: undefined,\n validate: undefined,\n errors: undefined,\n },\n)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFormFile' })\n\nconst rowClass = computed(() => rowGaps[resolvedSize.value])\nconst stackClass = computed(() => stackGaps[resolvedSize.value])\nconst textClass = computed(() => textSizes[resolvedSize.value])\nconst removeTextClass = computed(() => removeTextSizes[resolvedSize.value])\nconst iconOffsetClass = computed(() => iconOffsets[resolvedSize.value])\nconst buttonSize = computed(() => buttonSizes[resolvedSize.value])\nconst iconSize = computed(() => iconSizes[resolvedSize.value])\nconst previewClass = computed(() => [previewSizes[resolvedSize.value], previewBaseClass])\n\nconst emit = defineEmits<GrFormFileEmits>()\ndefineSlots<{\n /** Собственный вывод ошибок вместо списка по умолчанию. */\n error?: (props: { errors: GrFormFileError[] }) => any\n}>()\n\n\nconst { t } = useGranularityTranslations()\n\n// Контекст `GrFormField`. Виджет здесь — кнопка выбора файла, а не нативный\n// `<input type=\"file\">`: он `aria-hidden` и вне таб-порядка, id на нём увёл бы\n// `<label for>` в невидимый элемент.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n // `locked` — «ввод не принимается»: `disabled` или `readonly`. Набор в\n // `readonly` виден и уходит в форму, но поменять его нельзя ни диалогом, ни\n // перетаскиванием, ни удалением.\n locked: isLocked,\n} = useGrFormControl(() => props)\nconst resolvedUploadText = computed(() => props.uploadText ?? t('gr.formFile.upload', 'Upload file'))\nconst resolvedChangeText = computed(() => props.changeText ?? t('gr.formFile.change', 'Change file'))\nconst resolvedRemoveText = computed(() => props.removeText ?? t('gr.formFile.remove', 'Remove'))\nconst resolvedClearAllText = computed(() => props.clearAllText ?? t('gr.formFile.clearAll', 'Clear all'))\nconst resolvedPlaceholder = computed(() => props.placeholder ?? t('gr.formFile.placeholder', 'No files selected'))\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})\nconst localErrors = ref<GrFormFileError[]>([])\nconst uploadBtnEl = ref<HTMLElement | null>(null)\n\n// Собственные ошибки компонент обязан и объявить (`role=\"alert\"`), и связать с\n// контролом: до этого «уронил файл не того типа» для скринридера выглядело как\n// «ничего не произошло». Ошибка поля из `GrFormField` при этом остаётся —\n// `aria-describedby` держит оба id.\nconst errorsId = useId()\n// Контролируемый список сильнее внутреннего — как `sortKey` у `GrDataTable`.\nconst displayedErrors = computed(() => props.errors ?? localErrors.value)\nconst hasLocalErrors = computed(() => displayedErrors.value.length > 0)\n\n/**\n * `aria-required` и `aria-readonly` роль `button` не поддерживает — axe роняет\n * это как critical `aria-allowed-attr`. Состояния уходят в описание кнопки:\n * иначе они пропали бы для диктора совсем (маркер `*` у `GrFormField`\n * декоративен и скрыт).\n */\nconst stateHintId = useId()\nconst stateHint = computed(() => [\n isRequired.value ? t('gr.form.required', 'This field is required') : undefined,\n isReadonly.value ? t('gr.form.readonly', 'Read only') : undefined,\n].filter(Boolean).join('. ') || undefined)\n\nconst describedByIds = computed(() => {\n return [\n field?.describedById.value,\n hasLocalErrors.value ? errorsId : undefined,\n stateHint.value ? stateHintId : undefined,\n ]\n .filter(Boolean)\n .join(' ') || undefined\n})\n\nconst showsInvalid = computed(() => isInvalid.value || hasLocalErrors.value)\n\nfunction focus(): void {\n uploadBtnEl.value?.focus()\n}\n\nfunction blur(): void {\n uploadBtnEl.value?.blur()\n}\n\ndefineExpose({ focus, blur })\n\nconst files = computed<File[]>(() => {\n if (props.multiple) {\n return Array.isArray(props.modelValue) ? props.modelValue : []\n }\n\n return props.modelValue instanceof File ? [props.modelValue] : []\n})\n\nconst hasFiles = computed(() => files.value.length > 0)\n\nconst { fileKey, previewUrl, revokePreview, revokeAllPreviews } = useFilePreviews({\n enabled: () => props.preview,\n})\n\n/**\n * Отзыв `object URL` привязан к самому набору, а не к местам, где его меняют.\n * Файл уходит из набора десятком путей — кнопка строки, «очистить всё», новый\n * выбор, внешний сброс `v-model`, — и точечные вызовы разъехались бы с первой\n * же новой веткой; blob при этом висел бы в памяти вкладки до перезагрузки.\n */\nwatch(files, (next, prev) => {\n for (const file of prev ?? []) {\n if (!next.includes(file)) revokePreview(file)\n }\n})\n\nonBeforeUnmount(revokeAllPreviews)\n\n/**\n * Один набор валидаторов на оба пути ввода. Собери его отдельно в `applyFiles`\n * и в `dropzone` — копии разъедутся при первой же правке, и выбор через диалог\n * начнёт вести себя иначе, чем перетаскивание.\n */\nconst effectiveValidators = computed<FileValidator[]>(() => {\n const customValidator: FileValidator | undefined = props.validate\n ? async ({ files }) => (await props.validate?.(files)) ?? []\n : undefined\n\n return [\n acceptValidator(props.accept),\n maxCountValidator(props.limit),\n ...(props.validators ?? []),\n ...(customValidator ? [customValidator] : []),\n ]\n})\n\nfunction clearInputValue() {\n if (!inputRef.value) return\n inputRef.value.value = ''\n}\n\nfunction setErrors(next: GrFormFileError[]) {\n localErrors.value = next\n emit('update:errors', next)\n}\n\nfunction clearErrors() {\n if (displayedErrors.value.length === 0) return\n setErrors([])\n}\n\nfunction openDialog() {\n if (isLocked.value) return\n inputRef.value?.click()\n}\n\nfunction emitModel(nextFiles: File[]) {\n const value: File | File[] | null = props.multiple\n ? nextFiles\n : (nextFiles[0] ?? null)\n\n emit('update:modelValue', value)\n emit('change', value)\n}\n\nasync function applyFiles(nextFiles: File[]) {\n const isPromiseLike = (value: unknown): value is PromiseLike<any> => {\n return !!value && typeof (value as any).then === 'function'\n }\n\n const res = runFileValidators(nextFiles, effectiveValidators.value, {\n source: 'input',\n multiple: props.multiple,\n })\n\n const { files: picked, issues } = isPromiseLike(res) ? await res : res\n\n if (issues.length > 0) {\n setErrors(issues)\n clearInputValue()\n await nextTick()\n return\n }\n\n clearErrors()\n emitModel(picked)\n clearInputValue()\n await nextTick()\n}\n\nasync function onInputChange(event: Event) {\n if (isLocked.value) return\n\n const target = event.target as HTMLInputElement | null\n const nextFiles = target?.files ? Array.prototype.slice.call(target.files) as File[] : []\n\n // Reset value so selecting the same file twice triggers change.\n if (target) target.value = ''\n await nextTick()\n\n await applyFiles(nextFiles)\n}\n\nfunction clearAll() {\n if (isLocked.value) return\n clearErrors()\n clearInputValue()\n emit('clear')\n\n emit('update:modelValue', props.multiple ? [] : null)\n emit('change', props.multiple ? [] : null)\n}\n\nfunction removeAt(index: number) {\n if (isLocked.value) return\n if (!props.multiple) {\n clearAll()\n return\n }\n\n const next = files.value.slice()\n next.splice(index, 1)\n\n emit('update:modelValue', next)\n emit('change', next)\n}\n\n/** Размер файла в строке списка — как в `GrFileUpload`: без него список не отвечает «сколько». */\nfunction formatFileSize(file: File): string {\n return `${Math.ceil(file.size / 1024)} KB`\n}\n\nfunction issueMessage(issue: GrFormFileError): string {\n const text = resolveFileValidationMessage(issue, t)\n\n // Префикс с именем файла нужен только тем сообщениям, которые сами его не\n // называют, — то есть валидаторам потребителя. Встроенные передают `fileName`\n // параметром и подставляют его в текст, и приписка давала бы «photo.png:\n // Файл «photo.png» …». Признак — наличие параметра, а не поиск подстроки.\n if (issue.fileName && issue.i18nParams?.fileName === undefined)\n return `${issue.fileName}: ${text}`\n\n return text\n}\n\nconst dropzone = computed(() => {\n return {\n enabled: !isLocked.value,\n multiple: props.multiple,\n validators: effectiveValidators.value,\n onFiles: async (dropped: File[]) => {\n // `v-dropzone` уже выполнил валидаторы и нормализацию по `multiple`.\n clearErrors()\n emitModel(dropped)\n clearInputValue()\n await nextTick()\n },\n onError: (error: unknown) => {\n if (error instanceof FileValidationError) {\n setErrors(error.issues)\n }\n else {\n setErrors([\n {\n code: 'accept',\n message: t('gr.formFile.validateError', 'Failed to validate dropped files'),\n },\n ])\n }\n },\n }\n})\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (!inputRef.value) return\n\n if (!value || (Array.isArray(value) && value.length === 0)) {\n // Позволяет выбрать тот же файл снова после внешнего сброса.\n inputRef.value.value = ''\n }\n },\n)\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n v-dropzone=\"dropzone\"\n data-gr-form-file\n class=\"rounded-[var(--gr-radius-md)]\"\n :class=\"isDisabled ? 'cursor-not-allowed' : ''\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <span v-if=\"stateHint\" :id=\"stateHintId\" class=\"sr-only\">{{ stateHint }}</span>\n\n <input\n ref=\"inputRef\"\n data-gr-form-file-input\n type=\"file\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n class=\"absolute opacity-0 w-px h-px pointer-events-none\"\n :multiple=\"multiple\"\n :accept=\"accept\"\n :disabled=\"isDisabled\"\n @change=\"onInputChange\"\n >\n\n <div class=\"flex flex-col\" :class=\"stackClass\">\n <div class=\"flex flex-wrap items-center\" :class=\"rowClass\">\n <GrButton\n :id=\"fieldId\"\n ref=\"uploadBtnEl\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-upload-btn\n :aria-describedby=\"describedByIds\"\n :aria-invalid=\"showsInvalid ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :disabled=\"isDisabled\"\n @click.prevent=\"openDialog\"\n >\n <GrIcon :size=\"iconSize\">\n <IconUpload />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ hasFiles ? resolvedChangeText : resolvedUploadText }}</span>\n </GrButton>\n\n <GrButton\n v-if=\"hasFiles && !multiple && !isReadonly\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-clear-btn\n :disabled=\"isDisabled\"\n @click.prevent=\"clearAll\"\n >\n <GrIcon :size=\"iconSize\">\n <IconX />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ resolvedRemoveText }}</span>\n </GrButton>\n\n <GrButton\n v-if=\"multiple && hasFiles && !isReadonly\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-clear-all-btn\n :disabled=\"isDisabled\"\n @click.prevent=\"clearAll\"\n >\n <GrIcon :size=\"iconSize\">\n <IconX />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ resolvedClearAllText }}</span>\n </GrButton>\n\n <img\n v-if=\"!multiple && hasFiles && previewUrl(files[0]!)\"\n data-gr-form-file-preview\n :src=\"previewUrl(files[0]!)\"\n alt=\"\"\n :class=\"previewClass\"\n >\n\n <span\n v-if=\"!multiple && hasFiles\"\n class=\"text-[var(--gr-muted-fg)] truncate max-w-[240px]\"\n :class=\"textClass\"\n data-gr-form-file-single-name\n :title=\"files[0]?.name\"\n >\n {{ files[0]?.name }}\n </span>\n\n <span\n v-if=\"!hasFiles\"\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"textClass\"\n data-gr-form-file-placeholder\n >\n {{ resolvedPlaceholder }}\n </span>\n </div>\n\n <div v-if=\"multiple && hasFiles\" class=\"flex flex-col\" :class=\"stackClass\">\n <div\n v-for=\"(file, index) in files\"\n :key=\"fileKey(file)\"\n class=\"flex items-center gap-2\"\n data-gr-form-file-item\n >\n <img\n v-if=\"previewUrl(file)\"\n data-gr-form-file-preview\n :src=\"previewUrl(file)\"\n alt=\"\"\n :class=\"previewClass\"\n >\n\n <span\n class=\"text-[var(--gr-muted-fg)] truncate max-w-[240px]\"\n :class=\"textClass\"\n :title=\"file.name\"\n data-gr-form-file-item-name\n >\n {{ file.name }}\n </span>\n\n <span\n class=\"text-[var(--gr-muted-fg)] shrink-0\"\n :class=\"removeTextClass\"\n data-gr-form-file-item-size\n >{{ formatFileSize(file) }}</span>\n\n <button\n v-if=\"!isReadonly\"\n type=\"button\"\n class=\"text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)]\"\n :class=\"removeTextClass\"\n data-gr-form-file-item-remove\n :disabled=\"isDisabled\"\n :aria-label=\"t('gr.formFile.removeFile', 'Remove {fileName}', { fileName: file.name })\"\n @click.prevent=\"removeAt(index)\"\n >\n {{ resolvedRemoveText }}\n </button>\n </div>\n </div>\n\n <slot name=\"error\" :errors=\"displayedErrors\">\n <div\n v-if=\"hasLocalErrors\"\n :id=\"errorsId\"\n class=\"text-[var(--gr-danger-text)]\"\n :class=\"textClass\"\n data-gr-form-file-errors\n role=\"alert\"\n >\n <div v-for=\"(e, i) in displayedErrors\" :key=\"i\" data-gr-form-file-error>\n {{ issueMessage(e) }}\n </div>\n </div>\n </slot>\n </div>\n </div>\n</template>\n\n<style scoped>\n[data-gr-form-file].gr-dropzone--over {\n outline: 2px solid var(--gr-ring);\n outline-offset: 2px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, ref, useId, watch } from 'vue'\n\nimport IconUpload from '~icons/lucide/upload'\nimport IconX from '~icons/lucide/x'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n type GrFormFileSize,\n buttonSizes,\n iconOffsets,\n iconSizes,\n previewBaseClass,\n previewSizes,\n removeTextSizes,\n rowGaps,\n stackGaps,\n textSizes,\n} from './grFormFileStyles'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useFilePreviews } from '../../composables/internal/useFilePreviews'\nimport { vDropzone } from '../../directives'\nimport { acceptValidator, FileValidationError, maxCountValidator, resolveFileValidationMessage, runFileValidators } from '../../fileValidation'\nimport type { FileValidationIssue, FileValidator } from '../../fileValidation'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nexport type GrFormFileError = FileValidationIssue\n\n/**\n * Пропы `GrFormFile` — форма-поле для выбора файлов с встроенной валидацией,\n * drag&drop (через `v-dropzone`) и списком выбранных файлов.\n *\n * Все `*Text`/`placeholder` — i18n-friendly, принимают готовые строки локали.\n */\nexport interface GrFormFileProps {\n modelValue: File | File[] | null\n multiple?: 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 /** W3C `accept` для `<input type=\"file\">` + sugar к `acceptValidator(...)`. */\n accept?: string\n validators?: FileValidator[]\n /** Максимум файлов в наборе. Лишние не обрезаются молча — набор отбивается ошибкой. */\n limit?: number\n uploadText?: string\n changeText?: string\n removeText?: string\n clearAllText?: string\n placeholder?: string\n /** Размер кнопок, иконок и подписей. */\n size?: GrFormFileSize\n /** Миниатюры для картинок в наборе. Файлы других типов остаются строкой. */\n preview?: boolean\n /** Дополнительная (кастомная) валидация на стороне потребителя. */\n validate?: (files: File[]) => GrFormFileError[] | Promise<GrFormFileError[]>\n /**\n * Контролируемый список ошибок: `v-model:errors`. Задан — показывается он, и\n * внутренняя валидация его не перетирает. Сюда же кладутся ошибки, пришедшие\n * с сервера. Не задан — компонент держит свои ошибки сам.\n */\n errors?: GrFormFileError[]\n}\n\nexport interface GrFormFileEmits {\n (e: 'update:modelValue', value: File | File[] | null): void\n (e: 'change', value: File | File[] | null): void\n (e: 'clear'): void\n /** Результат валидации. Канал один: `validation` дублировал эту же нагрузку. */\n (e: 'update:errors', errors: GrFormFileError[]): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormFileProps>(),\n {\n multiple: false,\n preview: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n accept: undefined,\n limit: undefined,\n uploadText: undefined,\n changeText: undefined,\n removeText: undefined,\n clearAllText: undefined,\n placeholder: undefined,\n size: undefined,\n validators: undefined,\n validate: undefined,\n errors: undefined,\n },\n)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFormFile' })\n\nconst rowClass = computed(() => rowGaps[resolvedSize.value])\nconst stackClass = computed(() => stackGaps[resolvedSize.value])\nconst textClass = computed(() => textSizes[resolvedSize.value])\nconst removeTextClass = computed(() => removeTextSizes[resolvedSize.value])\nconst iconOffsetClass = computed(() => iconOffsets[resolvedSize.value])\nconst buttonSize = computed(() => buttonSizes[resolvedSize.value])\nconst iconSize = computed(() => iconSizes[resolvedSize.value])\nconst previewClass = computed(() => [previewSizes[resolvedSize.value], previewBaseClass])\n\nconst emit = defineEmits<GrFormFileEmits>()\ndefineSlots<{\n /** Собственный вывод ошибок вместо списка по умолчанию. */\n error?: (props: { errors: GrFormFileError[] }) => any\n}>()\n\n\nconst { t } = useGranularityTranslations()\n\n// Контекст `GrFormField`. Виджет здесь — кнопка выбора файла, а не нативный\n// `<input type=\"file\">`: он `aria-hidden` и вне таб-порядка, id на нём увёл бы\n// `<label for>` в невидимый элемент.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n // `locked` — «ввод не принимается»: `disabled` или `readonly`. Набор в\n // `readonly` виден и уходит в форму, но поменять его нельзя ни диалогом, ни\n // перетаскиванием, ни удалением.\n locked: isLocked,\n} = useGrFormControl(() => props)\nconst resolvedUploadText = computed(() => props.uploadText ?? t('gr.formFile.upload', 'Upload file'))\nconst resolvedChangeText = computed(() => props.changeText ?? t('gr.formFile.change', 'Change file'))\nconst resolvedRemoveText = computed(() => props.removeText ?? t('gr.formFile.remove', 'Remove'))\nconst resolvedClearAllText = computed(() => props.clearAllText ?? t('gr.formFile.clearAll', 'Clear all'))\nconst resolvedPlaceholder = computed(() => props.placeholder ?? t('gr.formFile.placeholder', 'No files selected'))\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})\nconst localErrors = ref<GrFormFileError[]>([])\nconst uploadBtnEl = ref<HTMLElement | null>(null)\n\n// Собственные ошибки компонент обязан и объявить (`role=\"alert\"`), и связать с\n// контролом: до этого «уронил файл не того типа» для скринридера выглядело как\n// «ничего не произошло». Ошибка поля из `GrFormField` при этом остаётся —\n// `aria-describedby` держит оба id.\nconst errorsId = useId()\n// Контролируемый список сильнее внутреннего — как `sortKey` у `GrDataTable`.\nconst displayedErrors = computed(() => props.errors ?? localErrors.value)\nconst hasLocalErrors = computed(() => displayedErrors.value.length > 0)\n\n/**\n * `aria-required` и `aria-readonly` роль `button` не поддерживает — axe роняет\n * это как critical `aria-allowed-attr`. Состояния уходят в описание кнопки:\n * иначе они пропали бы для диктора совсем (маркер `*` у `GrFormField`\n * декоративен и скрыт).\n */\nconst stateHintId = useId()\nconst stateHint = computed(() => [\n isRequired.value ? t('gr.form.required', 'This field is required') : undefined,\n isReadonly.value ? t('gr.form.readonly', 'Read only') : undefined,\n].filter(Boolean).join('. ') || undefined)\n\nconst describedByIds = computed(() => {\n return [\n field?.describedById.value,\n hasLocalErrors.value ? errorsId : undefined,\n stateHint.value ? stateHintId : undefined,\n ]\n .filter(Boolean)\n .join(' ') || undefined\n})\n\nconst showsInvalid = computed(() => isInvalid.value || hasLocalErrors.value)\n\nfunction focus(): void {\n uploadBtnEl.value?.focus()\n}\n\nfunction blur(): void {\n uploadBtnEl.value?.blur()\n}\n\ndefineExpose({ focus, blur })\n\nconst files = computed<File[]>(() => {\n if (props.multiple) {\n return Array.isArray(props.modelValue) ? props.modelValue : []\n }\n\n return props.modelValue instanceof File ? [props.modelValue] : []\n})\n\nconst hasFiles = computed(() => files.value.length > 0)\n\nconst { fileKey, previewUrl, revokePreview, revokeAllPreviews } = useFilePreviews({\n enabled: () => props.preview,\n})\n\n/**\n * Отзыв `object URL` привязан к самому набору, а не к местам, где его меняют.\n * Файл уходит из набора десятком путей — кнопка строки, «очистить всё», новый\n * выбор, внешний сброс `v-model`, — и точечные вызовы разъехались бы с первой\n * же новой веткой; blob при этом висел бы в памяти вкладки до перезагрузки.\n */\nwatch(files, (next, prev) => {\n for (const file of prev ?? []) {\n if (!next.includes(file)) revokePreview(file)\n }\n})\n\nonBeforeUnmount(revokeAllPreviews)\n\n/**\n * Один набор валидаторов на оба пути ввода. Собери его отдельно в `applyFiles`\n * и в `dropzone` — копии разъедутся при первой же правке, и выбор через диалог\n * начнёт вести себя иначе, чем перетаскивание.\n */\nconst effectiveValidators = computed<FileValidator[]>(() => {\n const customValidator: FileValidator | undefined = props.validate\n ? async ({ files }) => (await props.validate?.(files)) ?? []\n : undefined\n\n return [\n acceptValidator(props.accept),\n maxCountValidator(props.limit),\n ...(props.validators ?? []),\n ...(customValidator ? [customValidator] : []),\n ]\n})\n\nfunction clearInputValue() {\n if (!inputRef.value) return\n inputRef.value.value = ''\n}\n\nfunction setErrors(next: GrFormFileError[]) {\n localErrors.value = next\n emit('update:errors', next)\n}\n\nfunction clearErrors() {\n if (displayedErrors.value.length === 0) return\n setErrors([])\n}\n\nfunction openDialog() {\n if (isLocked.value) return\n inputRef.value?.click()\n}\n\nfunction emitModel(nextFiles: File[]) {\n const value: File | File[] | null = props.multiple\n ? nextFiles\n : (nextFiles[0] ?? null)\n\n emit('update:modelValue', value)\n emit('change', value)\n}\n\nasync function applyFiles(nextFiles: File[]) {\n const isPromiseLike = (value: unknown): value is PromiseLike<any> => {\n return !!value && typeof (value as any).then === 'function'\n }\n\n const res = runFileValidators(nextFiles, effectiveValidators.value, {\n source: 'input',\n multiple: props.multiple,\n })\n\n const { files: picked, issues } = isPromiseLike(res) ? await res : res\n\n if (issues.length > 0) {\n setErrors(issues)\n clearInputValue()\n await nextTick()\n return\n }\n\n clearErrors()\n emitModel(picked)\n clearInputValue()\n await nextTick()\n}\n\nasync function onInputChange(event: Event) {\n if (isLocked.value) return\n\n const target = event.target as HTMLInputElement | null\n const nextFiles = target?.files ? Array.prototype.slice.call(target.files) as File[] : []\n\n // Reset value so selecting the same file twice triggers change.\n if (target) target.value = ''\n await nextTick()\n\n await applyFiles(nextFiles)\n}\n\nfunction clearAll() {\n if (isLocked.value) return\n clearErrors()\n clearInputValue()\n emit('clear')\n\n emit('update:modelValue', props.multiple ? [] : null)\n emit('change', props.multiple ? [] : null)\n}\n\nfunction removeAt(index: number) {\n if (isLocked.value) return\n if (!props.multiple) {\n clearAll()\n return\n }\n\n const next = files.value.slice()\n next.splice(index, 1)\n\n emit('update:modelValue', next)\n emit('change', next)\n}\n\n/** Размер файла в строке списка — как в `GrFileUpload`: без него список не отвечает «сколько». */\nfunction formatFileSize(file: File): string {\n return `${Math.ceil(file.size / 1024)} KB`\n}\n\nfunction issueMessage(issue: GrFormFileError): string {\n const text = resolveFileValidationMessage(issue, t)\n\n // Префикс с именем файла нужен только тем сообщениям, которые сами его не\n // называют, — то есть валидаторам потребителя. Встроенные передают `fileName`\n // параметром и подставляют его в текст, и приписка давала бы «photo.png:\n // Файл «photo.png» …». Признак — наличие параметра, а не поиск подстроки.\n if (issue.fileName && issue.i18nParams?.fileName === undefined)\n return `${issue.fileName}: ${text}`\n\n return text\n}\n\nconst dropzone = computed(() => {\n return {\n enabled: !isLocked.value,\n multiple: props.multiple,\n validators: effectiveValidators.value,\n onFiles: async (dropped: File[]) => {\n // `v-dropzone` уже выполнил валидаторы и нормализацию по `multiple`.\n clearErrors()\n emitModel(dropped)\n clearInputValue()\n await nextTick()\n },\n onError: (error: unknown) => {\n if (error instanceof FileValidationError) {\n setErrors(error.issues)\n }\n else {\n setErrors([\n {\n code: 'accept',\n message: t('gr.formFile.validateError', 'Failed to validate dropped files'),\n },\n ])\n }\n },\n }\n})\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (!inputRef.value) return\n\n if (!value || (Array.isArray(value) && value.length === 0)) {\n // Позволяет выбрать тот же файл снова после внешнего сброса.\n inputRef.value.value = ''\n }\n },\n)\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n v-dropzone=\"dropzone\"\n data-gr-form-file\n class=\"rounded-[var(--gr-radius-md)]\"\n :class=\"isDisabled ? 'cursor-not-allowed' : ''\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <span v-if=\"stateHint\" :id=\"stateHintId\" class=\"sr-only\">{{ stateHint }}</span>\n\n <input\n ref=\"inputRef\"\n data-gr-form-file-input\n type=\"file\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n class=\"absolute opacity-0 w-px h-px pointer-events-none\"\n :multiple=\"multiple\"\n :accept=\"accept\"\n :disabled=\"isDisabled\"\n @change=\"onInputChange\"\n >\n\n <div class=\"flex flex-col\" :class=\"stackClass\">\n <div class=\"flex flex-wrap items-center\" :class=\"rowClass\">\n <GrButton\n :id=\"fieldId\"\n ref=\"uploadBtnEl\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-upload-btn\n :aria-describedby=\"describedByIds\"\n :aria-invalid=\"showsInvalid ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :disabled=\"isDisabled\"\n @click.prevent=\"openDialog\"\n >\n <GrIcon :size=\"iconSize\">\n <IconUpload />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ hasFiles ? resolvedChangeText : resolvedUploadText }}</span>\n </GrButton>\n\n <GrButton\n v-if=\"hasFiles && !multiple && !isReadonly\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-clear-btn\n :disabled=\"isDisabled\"\n @click.prevent=\"clearAll\"\n >\n <GrIcon :size=\"iconSize\">\n <IconX />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ resolvedRemoveText }}</span>\n </GrButton>\n\n <GrButton\n v-if=\"multiple && hasFiles && !isReadonly\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-clear-all-btn\n :disabled=\"isDisabled\"\n @click.prevent=\"clearAll\"\n >\n <GrIcon :size=\"iconSize\">\n <IconX />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ resolvedClearAllText }}</span>\n </GrButton>\n\n <img\n v-if=\"!multiple && hasFiles && previewUrl(files[0]!)\"\n data-gr-form-file-preview\n :src=\"previewUrl(files[0]!)\"\n alt=\"\"\n :class=\"previewClass\"\n >\n\n <span\n v-if=\"!multiple && hasFiles\"\n class=\"text-[var(--gr-muted-fg)] truncate max-w-[240px]\"\n :class=\"textClass\"\n data-gr-form-file-single-name\n :title=\"files[0]?.name\"\n >\n {{ files[0]?.name }}\n </span>\n\n <span\n v-if=\"!hasFiles\"\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"textClass\"\n data-gr-form-file-placeholder\n >\n {{ resolvedPlaceholder }}\n </span>\n </div>\n\n <div v-if=\"multiple && hasFiles\" class=\"flex flex-col\" :class=\"stackClass\">\n <div\n v-for=\"(file, index) in files\"\n :key=\"fileKey(file)\"\n class=\"flex items-center gap-2\"\n data-gr-form-file-item\n >\n <img\n v-if=\"previewUrl(file)\"\n data-gr-form-file-preview\n :src=\"previewUrl(file)\"\n alt=\"\"\n :class=\"previewClass\"\n >\n\n <span\n class=\"text-[var(--gr-muted-fg)] truncate max-w-[240px]\"\n :class=\"textClass\"\n :title=\"file.name\"\n data-gr-form-file-item-name\n >\n {{ file.name }}\n </span>\n\n <span\n class=\"text-[var(--gr-muted-fg)] shrink-0\"\n :class=\"removeTextClass\"\n data-gr-form-file-item-size\n >{{ formatFileSize(file) }}</span>\n\n <button\n v-if=\"!isReadonly\"\n type=\"button\"\n class=\"text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)]\"\n :class=\"removeTextClass\"\n data-gr-form-file-item-remove\n :disabled=\"isDisabled\"\n :aria-label=\"t('gr.formFile.removeFile', 'Remove {fileName}', { fileName: file.name })\"\n @click.prevent=\"removeAt(index)\"\n >\n {{ resolvedRemoveText }}\n </button>\n </div>\n </div>\n\n <slot name=\"error\" :errors=\"displayedErrors\">\n <div\n v-if=\"hasLocalErrors\"\n :id=\"errorsId\"\n class=\"text-[var(--gr-danger-text)]\"\n :class=\"textClass\"\n data-gr-form-file-errors\n role=\"alert\"\n >\n <div v-for=\"(e, i) in displayedErrors\" :key=\"i\" data-gr-form-file-error>\n {{ issueMessage(e) }}\n </div>\n </div>\n </slot>\n </div>\n </div>\n</template>\n\n<style scoped>\n[data-gr-form-file].gr-dropzone--over {\n outline: 2px solid var(--gr-ring);\n outline-offset: 2px;\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrFormFile_default } from "./chunks/GrFormFile-
|
|
1
|
+
import { t as GrFormFile_default } from "./chunks/GrFormFile-CYtXU9HV.js";
|
|
2
2
|
import { n as grFormFileSafelist, t as grFormFileConfig } from "../../chunks/config-BcXX7S7B.js";
|
|
3
3
|
export { GrFormFile_default as GrFormFile, GrFormFile_default as default, grFormFileConfig, grFormFileSafelist };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as useGrComponentSize } from "../../../chunks/context-CTbuwcYs.js";
|
|
2
|
-
import { a as iconSpinClass, i as iconSizeVarBySize, n as grIconToneClass, r as iconBaseClass } from "../../../chunks/grIconStyles-
|
|
2
|
+
import { a as iconSpinClass, i as iconSizeVarBySize, n as grIconToneClass, r as iconBaseClass } from "../../../chunks/grIconStyles-BwBrJY1E.js";
|
|
3
3
|
import { computed, createElementBlock, defineComponent, normalizeClass, normalizeStyle, openBlock, renderSlot } from "vue";
|
|
4
4
|
import '../styles.css';//#region src/components/GrIcon/GrIcon.vue?vue&type=script&setup=true&lang.ts
|
|
5
5
|
var _hoisted_1 = [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrIcon-
|
|
1
|
+
{"version":3,"file":"GrIcon-Dmf0c9ba.js","names":[],"sources":["../../../../src/components/GrIcon/GrIcon.vue","../../../../src/components/GrIcon/GrIcon.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n grIconToneClass,\n iconBaseClass,\n iconSizeVarBySize,\n iconSpinClass,\n type GrIconSize,\n type GrIconTone,\n} from './grIconStyles'\n\nexport type { GrIconSize, GrIconTone } from './grIconStyles'\n\nexport interface GrIconProps {\n /** Размер по шкале пакета либо произвольный в пикселях. */\n size?: GrIconSize | number\n /**\n * Имя значимой иконки. Задан — иконка объявляется `role=\"img\"` и перестаёт\n * быть скрытой. Не задан — иконка декоративна.\n */\n label?: string\n /** Цвет из палитры. `current` — наследовать цвет текста родителя. */\n tone?: GrIconTone\n /** Вращение — для спиннеров. */\n spin?: boolean\n}\n\nconst props = withDefaults(\n defineProps<GrIconProps>(),\n {\n size: undefined,\n label: undefined,\n tone: 'current',\n spin: false,\n },\n)\n\n// Число — «локальный» escape-hatch мимо конфига; шкала идёт через провайдер.\nconst resolvedScaleSize = useGrComponentSize(\n () => (typeof props.size === 'number' ? undefined : props.size),\n { component: 'GrIcon' },\n)\n\nconst iconStyle = computed(() => ({\n '--gr-icon-size': typeof props.size === 'number'\n ? `${props.size}px`\n : iconSizeVarBySize[resolvedScaleSize.value],\n} as Record<string, string>))\n\nconst iconClass = computed(() => [\n iconBaseClass,\n grIconToneClass(props.tone),\n props.spin ? iconSpinClass : '',\n].filter(Boolean))\n\n/**\n * Иконка декоративна по умолчанию.\n *\n * В библиотеке она почти всегда сопровождает текст, и каждое место вызова\n * ставило `aria-hidden` вручную. Достаточно забыть один раз — и диктор\n * прочитает `<title>` из SVG вместо ничего. Значимой иконку делает `label`:\n * тогда у неё появляются роль и имя, а скрытие снимается.\n *\n * Переопределить это потребитель по-прежнему может: fallthrough-атрибут\n * сильнее собственной привязки.\n */\nconst ariaHidden = computed(() => (props.label ? undefined : 'true'))\n\ndefineSlots<{\n /** Своя разметка иконки вместо имени из набора. */\n default?: () => any\n}>()\n\n</script>\n\n<template>\n <span\n data-gr-icon\n :class=\"iconClass\"\n :style=\"iconStyle\"\n :role=\"label ? 'img' : undefined\"\n :aria-label=\"label\"\n :aria-hidden=\"ariaHidden\"\n >\n <slot />\n </span>\n</template>\n\n<style>\n/*\n * Стиль намеренно глобальный, а не scoped: иконка приходит слотом (SVG\n * потребителя), и scoped-правило до него не достаёт — пришлось бы писать\n * `:deep` в каждом месте вызова.\n *\n * Размер живёт в одной переменной: и обёртка, и вложенный SVG считают себя от\n * неё, поэтому шкала настраивается темой, а не пропами разметки.\n */\n:where(.gr-icon) {\n --gr-icon-size: var(--gr-icon-size-md, 18px);\n width: var(--gr-icon-size);\n min-width: var(--gr-icon-size);\n height: var(--gr-icon-size);\n line-height: 0;\n flex: none;\n}\n\n:where(.gr-icon svg) {\n width: 100%;\n height: 100%;\n display: block;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n grIconToneClass,\n iconBaseClass,\n iconSizeVarBySize,\n iconSpinClass,\n type GrIconSize,\n type GrIconTone,\n} from './grIconStyles'\n\nexport type { GrIconSize, GrIconTone } from './grIconStyles'\n\nexport interface GrIconProps {\n /** Размер по шкале пакета либо произвольный в пикселях. */\n size?: GrIconSize | number\n /**\n * Имя значимой иконки. Задан — иконка объявляется `role=\"img\"` и перестаёт\n * быть скрытой. Не задан — иконка декоративна.\n */\n label?: string\n /** Цвет из палитры. `current` — наследовать цвет текста родителя. */\n tone?: GrIconTone\n /** Вращение — для спиннеров. */\n spin?: boolean\n}\n\nconst props = withDefaults(\n defineProps<GrIconProps>(),\n {\n size: undefined,\n label: undefined,\n tone: 'current',\n spin: false,\n },\n)\n\n// Число — «локальный» escape-hatch мимо конфига; шкала идёт через провайдер.\nconst resolvedScaleSize = useGrComponentSize(\n () => (typeof props.size === 'number' ? undefined : props.size),\n { component: 'GrIcon' },\n)\n\nconst iconStyle = computed(() => ({\n '--gr-icon-size': typeof props.size === 'number'\n ? `${props.size}px`\n : iconSizeVarBySize[resolvedScaleSize.value],\n} as Record<string, string>))\n\nconst iconClass = computed(() => [\n iconBaseClass,\n grIconToneClass(props.tone),\n props.spin ? iconSpinClass : '',\n].filter(Boolean))\n\n/**\n * Иконка декоративна по умолчанию.\n *\n * В библиотеке она почти всегда сопровождает текст, и каждое место вызова\n * ставило `aria-hidden` вручную. Достаточно забыть один раз — и диктор\n * прочитает `<title>` из SVG вместо ничего. Значимой иконку делает `label`:\n * тогда у неё появляются роль и имя, а скрытие снимается.\n *\n * Переопределить это потребитель по-прежнему может: fallthrough-атрибут\n * сильнее собственной привязки.\n */\nconst ariaHidden = computed(() => (props.label ? undefined : 'true'))\n\ndefineSlots<{\n /** Своя разметка иконки вместо имени из набора. */\n default?: () => any\n}>()\n\n</script>\n\n<template>\n <span\n data-gr-icon\n :class=\"iconClass\"\n :style=\"iconStyle\"\n :role=\"label ? 'img' : undefined\"\n :aria-label=\"label\"\n :aria-hidden=\"ariaHidden\"\n >\n <slot />\n </span>\n</template>\n\n<style>\n/*\n * Стиль намеренно глобальный, а не scoped: иконка приходит слотом (SVG\n * потребителя), и scoped-правило до него не достаёт — пришлось бы писать\n * `:deep` в каждом месте вызова.\n *\n * Размер живёт в одной переменной: и обёртка, и вложенный SVG считают себя от\n * неё, поэтому шкала настраивается темой, а не пропами разметки.\n */\n:where(.gr-icon) {\n --gr-icon-size: var(--gr-icon-size-md, 18px);\n width: var(--gr-icon-size);\n min-width: var(--gr-icon-size);\n height: var(--gr-icon-size);\n line-height: 0;\n flex: none;\n}\n\n:where(.gr-icon svg) {\n width: 100%;\n height: 100%;\n display: block;\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;EA6BA,MAAM,QAAQ;EAWd,MAAM,oBAAoB,yBACjB,OAAO,MAAM,SAAS,WAAW,KAAA,IAAY,MAAM,MAC1D,EAAE,WAAW,SAAS,CACxB;EAEA,MAAM,YAAY,gBAAgB,EAChC,kBAAkB,OAAO,MAAM,SAAS,WACpC,GAAG,MAAM,KAAK,MACd,kBAAkB,kBAAkB,OAC1C,EAA4B;EAE5B,MAAM,YAAY,eAAe;GAC/B;GACA,gBAAgB,MAAM,IAAI;GAC1B,MAAM,OAAO,gBAAgB;EAC/B,CAAC,CAAC,OAAO,OAAO,CAAC;;;;;;;;;;;;EAajB,MAAM,aAAa,eAAgB,MAAM,QAAQ,KAAA,IAAY,MAAO;;uBAUlE,mBASO,QAAA;IARL,gBAAA;IACC,OAAK,eAAE,UAAA,KAAS;IAChB,OAAK,eAAE,UAAA,KAAS;IAChB,MAAM,QAAA,QAAK,QAAW,KAAA;IACtB,cAAY,QAAA;IACZ,eAAa,WAAA;OAEd,WAAQ,KAAA,QAAA,SAAA,CAAA,GAAA,IAAA,UAAA"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrIcon_default } from "./chunks/GrIcon-
|
|
2
|
-
import { n as grIconSafelist, t as grIconConfig } from "../../chunks/config-
|
|
1
|
+
import { t as GrIcon_default } from "./chunks/GrIcon-Dmf0c9ba.js";
|
|
2
|
+
import { n as grIconSafelist, t as grIconConfig } from "../../chunks/config-3bQeQvqd.js";
|
|
3
3
|
export { GrIcon_default as GrIcon, GrIcon_default as default, grIconConfig, grIconSafelist };
|
package/dist/components/GrImageViewer/chunks/{GrImageViewer-COUcpOgR.js → GrImageViewer-CyVN6Zpc.js}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as x_default } from "../../../chunks/x-2KXnqxiD.js";
|
|
2
2
|
import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
|
|
3
|
-
import { t as GrIcon_default } from "../../GrIcon/chunks/GrIcon-
|
|
3
|
+
import { t as GrIcon_default } from "../../GrIcon/chunks/GrIcon-Dmf0c9ba.js";
|
|
4
4
|
import { t as minus_default } from "../../../chunks/minus-oxdbHF33.js";
|
|
5
5
|
import { useAnnouncer } from "../../../composables/useAnnouncer.js";
|
|
6
6
|
import { t as useModalOverlay } from "../../../chunks/useModalOverlay-Cv5hF63W.js";
|