@feugene/granularity 0.24.1 → 0.26.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-BViooYNv.js.map +1 -0
- package/dist/chunks/config-BfsBepkH.js.map +1 -0
- package/dist/chunks/config-BuGlNPE92.js.map +1 -0
- package/dist/chunks/config-CPSxM7XE.js +8 -0
- package/dist/chunks/config-CPSxM7XE.js.map +1 -0
- package/dist/chunks/config-D7zgE_Gr.js +107 -0
- package/dist/chunks/config-D7zgE_Gr.js.map +1 -0
- package/dist/chunks/config-DAX8Deh1.js.map +1 -0
- package/dist/chunks/config-DGCMHPCR.js +19 -0
- package/dist/chunks/config-DGCMHPCR.js.map +1 -0
- package/dist/chunks/config-DGWFal5h.js +128 -0
- package/dist/chunks/config-DGWFal5h.js.map +1 -0
- package/dist/chunks/config-DKISz9gR.js +39 -0
- package/dist/chunks/config-DKISz9gR.js.map +1 -0
- package/dist/chunks/{config-BSyiFlbi2.js → config-DtgYuGUn2.js} +2 -1
- package/dist/chunks/config-DtgYuGUn2.js.map +1 -0
- package/dist/chunks/config-Iu684VNI.js +21 -0
- package/dist/chunks/config-Iu684VNI.js.map +1 -0
- package/dist/chunks/config-T3qdQBgU.js.map +1 -0
- package/dist/chunks/config-rkcKArZQ.js.map +1 -0
- package/dist/chunks/config-vSji8w90.js.map +1 -0
- package/dist/chunks/config-zjeAJ4ho2.js.map +1 -0
- package/dist/chunks/context-BfIQv7m12.js.map +1 -0
- package/dist/chunks/context-CTbuwcYs.js.map +1 -1
- package/dist/chunks/{en-DCCFP4qu.js → en-NpP9e7lC.js} +16 -2
- package/dist/chunks/en-NpP9e7lC.js.map +1 -0
- package/dist/chunks/{es-CT34HBVM.js → es-DaeA2Ql-.js} +16 -2
- package/dist/chunks/es-DaeA2Ql-.js.map +1 -0
- package/dist/chunks/{es-DdJsxlUr.js → es-Do2CNrC2.js} +3 -3
- package/dist/chunks/{es-DdJsxlUr.js.map → es-Do2CNrC2.js.map} +1 -1
- package/dist/chunks/{generated-1WEi10KK.js → generated-CJe6hdiE.js} +49 -0
- package/dist/chunks/{generated-1WEi10KK.js.map → generated-CJe6hdiE.js.map} +1 -1
- package/dist/chunks/{grBadgeStyles-C2wLlkjF.js → grBadgeStyles-C1S56Alo.js} +6 -1
- package/dist/chunks/{grBadgeStyles-C2wLlkjF.js.map → grBadgeStyles-C1S56Alo.js.map} +1 -1
- package/dist/chunks/grChipGroupContext-DZXkACap.js +4 -0
- package/dist/chunks/grChipGroupContext-DZXkACap.js.map +1 -0
- package/dist/chunks/{config-C5mO5DYZ.js → grDropdownMenuStyles-D1LOkdVj.js} +1 -38
- package/dist/chunks/grDropdownMenuStyles-D1LOkdVj.js.map +1 -0
- package/dist/chunks/{grPopoverStyles-Rz_Z5nLW.js → grPopoverStyles-Cke_BZsM.js} +16 -3
- package/dist/chunks/grPopoverStyles-Cke_BZsM.js.map +1 -0
- package/dist/chunks/{granular-provider-Dr2PKgl1.js → granular-provider-BaHkLjFX.js} +19 -11
- package/dist/chunks/granular-provider-BaHkLjFX.js.map +1 -0
- package/dist/chunks/{ru-CLiAMwrx.js → ru-Bg9-DPmg.js} +16 -2
- package/dist/chunks/ru-Bg9-DPmg.js.map +1 -0
- package/dist/chunks/{config-qVg3Zz_A.js → safelist-DOfCQR5O.js} +2 -19
- package/dist/chunks/safelist-DOfCQR5O.js.map +1 -0
- package/dist/chunks/useMenuItemsFocus-OyOnIAlr.js +117 -0
- package/dist/chunks/useMenuItemsFocus-OyOnIAlr.js.map +1 -0
- package/dist/chunks/{validation-DauXRIkL.js → validation-5HhpanPP.js} +1 -1
- package/dist/chunks/{validation-DauXRIkL.js.map → validation-5HhpanPP.js.map} +1 -1
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-DsevPtiS.js → GrAutocomplete-D9c9BNFg.js} +1 -1
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-DsevPtiS.js.map → GrAutocomplete-D9c9BNFg.js.map} +1 -1
- package/dist/components/GrAutocomplete/index.js +1 -1
- package/dist/components/GrBadge/chunks/{GrBadge-u2ZdS7CN.js → GrBadge-_9rCHuE1.js} +1 -1
- package/dist/components/GrBadge/chunks/{GrBadge-u2ZdS7CN.js.map → GrBadge-_9rCHuE1.js.map} +1 -1
- package/dist/components/GrBadge/index.js +3 -2
- package/dist/components/GrButton/chunks/{GrButton-Bw2AH4uX.js → GrButton-DCz-Wn-V.js} +1 -1
- package/dist/components/GrButton/chunks/{GrButton-Bw2AH4uX.js.map → GrButton-DCz-Wn-V.js.map} +1 -1
- package/dist/components/GrButton/index.js +1 -1
- package/dist/components/GrButtonGroup/chunks/{GrButtonGroup-CKyiAV8K.js → GrButtonGroup-BQo5VQ_c.js} +1 -1
- package/dist/components/GrButtonGroup/chunks/{GrButtonGroup-CKyiAV8K.js.map → GrButtonGroup-BQo5VQ_c.js.map} +1 -1
- package/dist/components/GrButtonGroup/index.js +2 -2
- package/dist/components/GrCard/chunks/{GrCard-gI2wA8ZZ.js → GrCard-aQIjk6I9.js} +1 -1
- package/dist/components/GrCard/chunks/GrCard-aQIjk6I9.js.map +1 -0
- package/dist/components/GrCard/index.js +1 -1
- package/dist/components/GrCheckboxGroup/index.js +1 -1
- package/dist/components/GrChip/chunks/GrChip-BnL2FedO.js +222 -0
- package/dist/components/GrChip/chunks/GrChip-BnL2FedO.js.map +1 -0
- package/dist/components/GrChip/index.js +3 -0
- package/dist/components/GrChipGroup/chunks/GrChipGroup-DefmSdY9.js +209 -0
- package/dist/components/GrChipGroup/chunks/GrChipGroup-DefmSdY9.js.map +1 -0
- package/dist/components/GrChipGroup/index.js +3 -0
- package/dist/components/GrCodeBlock/chunks/{GrCodeBlock-BmXCLgLS.js → GrCodeBlock-CyptiEo4.js} +1 -1
- package/dist/components/GrCodeBlock/chunks/{GrCodeBlock-BmXCLgLS.js.map → GrCodeBlock-CyptiEo4.js.map} +1 -1
- package/dist/components/GrCodeBlock/index.js +1 -1
- package/dist/components/GrCollapse/chunks/{GrCollapse-DIHEiTju.js → GrCollapse-DUROgAgc.js} +1 -1
- package/dist/components/GrCollapse/chunks/{GrCollapse-DIHEiTju.js.map → GrCollapse-DUROgAgc.js.map} +1 -1
- package/dist/components/GrCollapse/index.js +1 -1
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-qQdoEPHF.js → GrColorPicker-CDaJRVIM.js} +1 -1
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-qQdoEPHF.js.map → GrColorPicker-CDaJRVIM.js.map} +1 -1
- package/dist/components/GrColorPicker/index.js +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-BFcMf4Pn.js → GrCommandPalette-BPsVN_XU.js} +3 -3
- package/dist/components/GrCommandPalette/chunks/GrCommandPalette-BPsVN_XU.js.map +1 -0
- package/dist/components/GrCommandPalette/index.js +1 -1
- package/dist/components/GrCommandPalette/styles.css +1 -1
- package/dist/components/GrConfigProvider/chunks/{GrConfigProvider-UP-EaoAf.js → GrConfigProvider-BF7R3hgg.js} +1 -1
- package/dist/components/GrConfigProvider/chunks/GrConfigProvider-BF7R3hgg.js.map +1 -0
- package/dist/components/GrConfigProvider/index.js +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-rRtMScJX.js → GrConfirmDialog-CwXNLyzk.js} +3 -3
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-rRtMScJX.js.map → GrConfirmDialog-CwXNLyzk.js.map} +1 -1
- package/dist/components/GrConfirmDialog/index.js +1 -1
- package/dist/components/GrContextMenu/chunks/GrContextMenu-BbAJholy.js +331 -0
- package/dist/components/GrContextMenu/chunks/GrContextMenu-BbAJholy.js.map +1 -0
- package/dist/components/GrContextMenu/index.js +3 -0
- package/dist/components/GrDataTable/chunks/{GrDataTable-B5MmZFUg.js → GrDataTable-NRbrXpdx.js} +2 -2
- package/dist/components/GrDataTable/chunks/GrDataTable-NRbrXpdx.js.map +1 -0
- package/dist/components/GrDataTable/index.js +1 -1
- package/dist/components/GrDescriptionList/chunks/{GrDescriptionList-C6Pxjjbh.js → GrDescriptionList-1mICfrqX.js} +1 -1
- package/dist/components/GrDescriptionList/chunks/{GrDescriptionList-C6Pxjjbh.js.map → GrDescriptionList-1mICfrqX.js.map} +1 -1
- package/dist/components/GrDescriptionList/index.js +2 -2
- package/dist/components/GrDialog/chunks/{GrDialog-D0U5iRqV.js → GrDialog-5Y5K87ss.js} +2 -2
- package/dist/components/GrDialog/chunks/{GrDialog-D0U5iRqV.js.map → GrDialog-5Y5K87ss.js.map} +1 -1
- package/dist/components/GrDialog/index.js +1 -1
- package/dist/components/GrDialogService/chunks/{GrDialogService-CrwA4aJq.js → GrDialogService-CHVR4sOk.js} +6 -6
- package/dist/components/GrDialogService/chunks/GrDialogService-CHVR4sOk.js.map +1 -0
- package/dist/components/GrDialogService/index.js +1 -1
- package/dist/components/GrDrawer/chunks/{GrDrawer-C3zAg2tG.js → GrDrawer-CXy3-SVd.js} +2 -2
- package/dist/components/GrDrawer/chunks/GrDrawer-CXy3-SVd.js.map +1 -0
- package/dist/components/GrDrawer/index.js +1 -1
- package/dist/components/GrDropdown/chunks/{GrDropdown-BSfFOcDx.js → GrDropdown-BfTmdPyh.js} +10 -79
- package/dist/components/GrDropdown/chunks/GrDropdown-BfTmdPyh.js.map +1 -0
- package/dist/components/GrDropdown/index.js +2 -2
- package/dist/components/GrDropdownMenu/chunks/GrDropdownMenu-l5HEqzjr.js +130 -0
- package/dist/components/GrDropdownMenu/chunks/GrDropdownMenu-l5HEqzjr.js.map +1 -0
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-DyS6dWDp.js → GrDropdownMenuList-BGA_xNVo.js} +57 -159
- package/dist/components/GrDropdownMenu/chunks/GrDropdownMenuList-BGA_xNVo.js.map +1 -0
- package/dist/components/GrDropdownMenu/index.js +3 -2
- package/dist/components/GrEmptyState/chunks/{GrEmptyState-BGUX8O0U.js → GrEmptyState-CS9do8eM.js} +1 -1
- package/dist/components/GrEmptyState/chunks/{GrEmptyState-BGUX8O0U.js.map → GrEmptyState-CS9do8eM.js.map} +1 -1
- package/dist/components/GrEmptyState/index.js +2 -2
- package/dist/components/GrFileUpload/chunks/{GrFileUpload-D3-vC-ap.js → GrFileUpload-wNXzHb5N.js} +1 -1
- package/dist/components/GrFileUpload/chunks/GrFileUpload-wNXzHb5N.js.map +1 -0
- package/dist/components/GrFileUpload/index.js +1 -1
- package/dist/components/GrForm/chunks/{GrForm-B_Qq_13k.js → GrForm-CAlJg-wb.js} +1 -1
- package/dist/components/GrForm/chunks/{GrForm-B_Qq_13k.js.map → GrForm-CAlJg-wb.js.map} +1 -1
- package/dist/components/GrForm/index.js +3 -3
- package/dist/components/GrFormField/chunks/{GrFormField-kqfVY30N.js → GrFormField-DSylU7IU.js} +1 -1
- package/dist/components/GrFormField/chunks/GrFormField-DSylU7IU.js.map +1 -0
- package/dist/components/GrFormField/index.js +1 -1
- package/dist/components/GrFormFile/chunks/{GrFormFile-D722xHn8.js → GrFormFile-DhVkB2gm.js} +1 -1
- package/dist/components/GrFormFile/chunks/{GrFormFile-D722xHn8.js.map → GrFormFile-DhVkB2gm.js.map} +1 -1
- package/dist/components/GrFormFile/index.js +1 -1
- package/dist/components/GrInputTag/chunks/{GrInputTag-C7QVPijj.js → GrInputTag-UI8kIB_F.js} +2 -2
- package/dist/components/GrInputTag/chunks/{GrInputTag-C7QVPijj.js.map → GrInputTag-UI8kIB_F.js.map} +1 -1
- package/dist/components/GrInputTag/index.js +1 -1
- package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-DFUppRNb.js → GrJsonViewer-jk3ylOTc.js} +2 -2
- package/dist/components/GrJsonViewer/chunks/GrJsonViewer-jk3ylOTc.js.map +1 -0
- package/dist/components/GrJsonViewer/index.js +1 -1
- package/dist/components/GrList/chunks/{GrList-DAtzVIHh.js → GrList-B5bclmHV.js} +30 -3
- package/dist/components/GrList/chunks/GrList-B5bclmHV.js.map +1 -0
- package/dist/components/GrList/index.js +1 -1
- package/dist/components/GrList/styles.css +1 -1
- package/dist/components/GrModal/chunks/{GrModal-CPz7SWD_.js → GrModal-CCoWZuIl.js} +2 -2
- package/dist/components/GrModal/chunks/GrModal-CCoWZuIl.js.map +1 -0
- package/dist/components/GrModal/index.js +1 -1
- package/dist/components/GrNavbar/chunks/{GrNavbar-DWaaoV7n.js → GrNavbar-DPj30sHX.js} +1 -1
- package/dist/components/GrNavbar/chunks/{GrNavbar-DWaaoV7n.js.map → GrNavbar-DPj30sHX.js.map} +1 -1
- package/dist/components/GrNavbar/index.js +1 -1
- package/dist/components/GrNumberInput/chunks/{GrNumberInput-BotnzW77.js → GrNumberInput-V2mZvDDC.js} +1 -1
- package/dist/components/GrNumberInput/chunks/{GrNumberInput-BotnzW77.js.map → GrNumberInput-V2mZvDDC.js.map} +1 -1
- package/dist/components/GrNumberInput/index.js +2 -2
- package/dist/components/GrPagination/chunks/{GrPagination-GtGtSMJG.js → GrPagination-DG7kp6VL.js} +2 -2
- package/dist/components/GrPagination/chunks/{GrPagination-GtGtSMJG.js.map → GrPagination-DG7kp6VL.js.map} +1 -1
- package/dist/components/GrPagination/index.js +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-b70bIlQ9.js → GrPopover-DDChM1jD.js} +28 -10
- package/dist/components/GrPopover/chunks/GrPopover-DDChM1jD.js.map +1 -0
- package/dist/components/GrPopover/index.js +2 -2
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-CEI8bf-t.js → GrPromptDialog-BGuMUzlv.js} +5 -5
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-CEI8bf-t.js.map → GrPromptDialog-BGuMUzlv.js.map} +1 -1
- package/dist/components/GrPromptDialog/index.js +1 -1
- package/dist/components/GrRadioGroup/chunks/{GrRadioGroup-BVmjKXcv.js → GrRadioGroup-7w4bXIg_.js} +1 -1
- package/dist/components/GrRadioGroup/chunks/{GrRadioGroup-BVmjKXcv.js.map → GrRadioGroup-7w4bXIg_.js.map} +1 -1
- package/dist/components/GrRadioGroup/index.js +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-CZONq8uT.js → GrResponseErrorBanner-BhP-xw2P.js} +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-CZONq8uT.js.map → GrResponseErrorBanner-BhP-xw2P.js.map} +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-CgfAwlg3.js → GrResponseErrorBanner-DA4q90Zt.js} +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-CgfAwlg3.js.map → GrResponseErrorBanner-DA4q90Zt.js.map} +1 -1
- package/dist/components/GrResponseErrorBanner/index.js +2 -2
- package/dist/components/GrSelect/chunks/{GrSelect-CaGuyvtb.js → GrSelect-BA1wfX3c.js} +19 -3
- package/dist/components/GrSelect/chunks/GrSelect-BA1wfX3c.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-D7WxRtIu.js → GrSidebar-DY-LgIbS.js} +2 -2
- package/dist/components/GrSidebar/chunks/{GrSidebar-D7WxRtIu.js.map → GrSidebar-DY-LgIbS.js.map} +1 -1
- package/dist/components/GrSidebar/index.js +2 -2
- package/dist/components/GrSlider/index.js +1 -1
- package/dist/components/GrSortableList/chunks/{GrSortableList-BmcUVAMb.js → GrSortableList-BMXyxmRc.js} +3 -3
- package/dist/components/GrSortableList/chunks/{GrSortableList-BmcUVAMb.js.map → GrSortableList-BMXyxmRc.js.map} +1 -1
- package/dist/components/GrSortableList/index.js +2 -2
- package/dist/components/GrSteps/chunks/GrSteps-Cce4rfr3.js +282 -0
- package/dist/components/GrSteps/chunks/GrSteps-Cce4rfr3.js.map +1 -0
- package/dist/components/GrSteps/index.js +3 -0
- package/dist/components/GrTabPanels/chunks/{GrTabPanels-AJt1w1XC.js → GrTabPanels-BZQS8-JM.js} +12 -1
- package/dist/components/GrTabPanels/chunks/{GrTabPanels-AJt1w1XC.js.map → GrTabPanels-BZQS8-JM.js.map} +1 -1
- package/dist/components/GrTabPanels/index.js +1 -1
- package/dist/components/GrTimeline/chunks/{GrTimeline-Ch9sHzwL.js → GrTimeline-DIDdkTn7.js} +12 -1
- package/dist/components/GrTimeline/chunks/GrTimeline-DIDdkTn7.js.map +1 -0
- package/dist/components/GrTimeline/index.js +1 -1
- package/dist/components/GrToaster/chunks/{GrToaster-DJUzx3sR.js → GrToaster-CC49q3a4.js} +1 -1
- package/dist/components/GrToaster/chunks/{GrToaster-DJUzx3sR.js.map → GrToaster-CC49q3a4.js.map} +1 -1
- package/dist/components/GrToaster/index.js +1 -1
- package/dist/components/GrTree/chunks/{GrTree-BvOeQ3xJ.js.map → GrTree-J5q8aCya.js.map} +1 -1
- package/dist/components/GrTree/index.js +1 -1
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-_LjTRR7X.js → GrTreeSelect-DEfxWAc3.js} +1 -1
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-_LjTRR7X.js.map → GrTreeSelect-DEfxWAc3.js.map} +1 -1
- package/dist/components/GrTreeSelect/index.js +1 -1
- package/dist/composables/useFloating.js +47 -7
- package/dist/composables/useFloating.js.map +1 -1
- package/dist/composables/useTheme.js.map +1 -1
- package/dist/composables/useToast.js.map +1 -1
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/i18n/all.js +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/index.js +68 -58
- package/dist/theme.js +1 -1
- package/dist/tokens.js +1 -1
- package/dist/types/components/GrBadge/grBadgeStyles.d.ts +7 -0
- package/dist/types/components/GrCard/GrCard.vue.d.ts +1 -1
- package/dist/types/components/GrChip/GrChip.vue.d.ts +118 -0
- package/dist/types/components/GrChip/config.d.ts +1 -0
- package/dist/types/components/GrChip/defaults.d.ts +16 -0
- package/dist/types/components/GrChip/grChipGroupContext.d.ts +52 -0
- package/dist/types/components/GrChip/grChipStyles.d.ts +64 -0
- package/dist/types/components/GrChip/index.d.ts +11 -0
- package/dist/types/components/GrChip/safelist.d.ts +7 -0
- package/dist/types/components/GrChipGroup/GrChipGroup.vue.d.ts +128 -0
- package/dist/types/components/GrChipGroup/config.d.ts +1 -0
- package/dist/types/components/GrChipGroup/defaults.d.ts +13 -0
- package/dist/types/components/GrChipGroup/grChipGroupStyles.d.ts +7 -0
- package/dist/types/components/GrChipGroup/index.d.ts +9 -0
- package/dist/types/components/GrChipGroup/safelist.d.ts +1 -0
- package/dist/types/components/GrContextMenu/GrContextMenu.vue.d.ts +157 -0
- package/dist/types/components/GrContextMenu/config.d.ts +1 -0
- package/dist/types/components/GrContextMenu/contextMenuAnchor.d.ts +29 -0
- package/dist/types/components/GrContextMenu/index.d.ts +7 -0
- package/dist/types/components/GrDataTable/GrDataTable.vue.d.ts +1 -1
- package/dist/types/components/GrDropdownMenu/GrDropdownMenu.vue.d.ts +3 -3
- package/dist/types/components/GrDropdownMenu/GrDropdownMenuEntries.vue.d.ts +22 -0
- package/dist/types/components/GrFilePreview/GrFilePreview.vue.d.ts +1 -1
- package/dist/types/components/GrForm/index.d.ts +1 -1
- package/dist/types/components/GrList/GrListItem.vue.d.ts +1 -1
- package/dist/types/components/GrPopover/GrPopover.vue.d.ts +21 -2
- package/dist/types/components/GrPopover/grPopoverStyles.d.ts +11 -1
- package/dist/types/components/GrPopover/index.d.ts +1 -1
- package/dist/types/components/GrStatistic/GrStatistic.vue.d.ts +1 -1
- package/dist/types/components/GrSteps/GrSteps.vue.d.ts +118 -0
- package/dist/types/components/GrSteps/config.d.ts +1 -0
- package/dist/types/components/GrSteps/defaults.d.ts +18 -0
- package/dist/types/components/GrSteps/grStepsStyles.d.ts +63 -0
- package/dist/types/components/GrSteps/index.d.ts +11 -0
- package/dist/types/components/GrSteps/safelist.d.ts +1 -0
- package/dist/types/components/GrSteps/stepsModel.d.ts +57 -0
- package/dist/types/composables/internal/useMenuItemsFocus.d.ts +24 -0
- package/dist/types/composables/internal/useTypeahead.d.ts +30 -0
- package/dist/types/composables/useFloating.d.ts +33 -2
- package/dist/types/granular-provider/shared.d.ts +4 -0
- package/dist/types/i18n/messages/en.d.ts +26 -0
- package/dist/types/i18n/messages/es.d.ts +26 -0
- package/dist/types/i18n/messages/ru.d.ts +26 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/web-types.json +790 -241
- package/package.json +18 -2
- package/dist/chunks/config-BSyiFlbi2.js.map +0 -1
- package/dist/chunks/config-BViooYNv2.js.map +0 -1
- package/dist/chunks/config-BfsBepkH2.js.map +0 -1
- package/dist/chunks/config-BuGlNPE9.js.map +0 -1
- package/dist/chunks/config-C5mO5DYZ.js.map +0 -1
- package/dist/chunks/config-DAX8Deh12.js.map +0 -1
- package/dist/chunks/config-T3qdQBgU2.js.map +0 -1
- package/dist/chunks/config-qVg3Zz_A.js.map +0 -1
- package/dist/chunks/config-rkcKArZQ2.js.map +0 -1
- package/dist/chunks/config-vSji8w902.js.map +0 -1
- package/dist/chunks/config-zjeAJ4ho.js.map +0 -1
- package/dist/chunks/context-BfIQv7m1.js.map +0 -1
- package/dist/chunks/en-DCCFP4qu.js.map +0 -1
- package/dist/chunks/es-CT34HBVM.js.map +0 -1
- package/dist/chunks/grPopoverStyles-Rz_Z5nLW.js.map +0 -1
- package/dist/chunks/granular-provider-Dr2PKgl1.js.map +0 -1
- package/dist/chunks/ru-CLiAMwrx.js.map +0 -1
- package/dist/components/GrCard/chunks/GrCard-gI2wA8ZZ.js.map +0 -1
- package/dist/components/GrCommandPalette/chunks/GrCommandPalette-BFcMf4Pn.js.map +0 -1
- package/dist/components/GrConfigProvider/chunks/GrConfigProvider-UP-EaoAf.js.map +0 -1
- package/dist/components/GrDataTable/chunks/GrDataTable-B5MmZFUg.js.map +0 -1
- package/dist/components/GrDialogService/chunks/GrDialogService-CrwA4aJq.js.map +0 -1
- package/dist/components/GrDrawer/chunks/GrDrawer-C3zAg2tG.js.map +0 -1
- package/dist/components/GrDropdown/chunks/GrDropdown-BSfFOcDx.js.map +0 -1
- package/dist/components/GrDropdownMenu/chunks/GrDropdownMenu-DyS6dWDp.js.map +0 -1
- package/dist/components/GrFileUpload/chunks/GrFileUpload-D3-vC-ap.js.map +0 -1
- package/dist/components/GrFormField/chunks/GrFormField-kqfVY30N.js.map +0 -1
- package/dist/components/GrJsonViewer/chunks/GrJsonViewer-DFUppRNb.js.map +0 -1
- package/dist/components/GrList/chunks/GrList-DAtzVIHh.js.map +0 -1
- package/dist/components/GrModal/chunks/GrModal-CPz7SWD_.js.map +0 -1
- package/dist/components/GrPopover/chunks/GrPopover-b70bIlQ9.js.map +0 -1
- package/dist/components/GrSelect/chunks/GrSelect-CaGuyvtb.js.map +0 -1
- package/dist/components/GrTimeline/chunks/GrTimeline-Ch9sHzwL.js.map +0 -1
- package/dist/chunks/{config-BViooYNv2.js → config-BViooYNv.js} +0 -0
- package/dist/chunks/{config-BfsBepkH2.js → config-BfsBepkH.js} +0 -0
- package/dist/chunks/{config-BuGlNPE9.js → config-BuGlNPE92.js} +0 -0
- package/dist/chunks/{config-DAX8Deh12.js → config-DAX8Deh1.js} +0 -0
- package/dist/chunks/{config-T3qdQBgU2.js → config-T3qdQBgU.js} +0 -0
- package/dist/chunks/{config-rkcKArZQ2.js → config-rkcKArZQ.js} +0 -0
- package/dist/chunks/{config-vSji8w902.js → config-vSji8w90.js} +0 -0
- package/dist/chunks/{config-zjeAJ4ho.js → config-zjeAJ4ho2.js} +0 -0
- package/dist/chunks/{context-BfIQv7m1.js → context-BfIQv7m12.js} +0 -0
- package/dist/components/GrTree/chunks/{GrTree-BvOeQ3xJ.js → GrTree-J5q8aCya.js} +1 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrFileUpload-D3-vC-ap.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) 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) 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()) return\n\n event.preventDefault()\n overCounter += 1\n setOver(true)\n }\n\n function onDragOver(event: DragEvent): void {\n if (ignored()) return\n\n event.preventDefault()\n if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'\n }\n\n function onDragLeave(event: DragEvent): void {\n if (ignored()) return\n\n event.preventDefault()\n overCounter = Math.max(0, overCounter - 1)\n if (overCounter === 0) setOver(false)\n }\n\n function onDropFiles(event: DragEvent): void {\n if (ignored()) 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) 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 } 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 } 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) 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) 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) 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)) 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) 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') 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') 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\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) return true\n\n for (const node of meaningful) {\n if (node.type !== Text) 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) return\n if (next === lastFiles.value) 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\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) 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') return undefined\n if (!Number.isFinite(limit)) return undefined\n if (limit <= 0) return undefined\n return Math.floor(limit)\n}\n\n\nasync function uploadViaAction(files: File[], signal: AbortSignal, extraData: GrFileUploadExtraData | undefined) {\n if (!props.action) 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) 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 } 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) return\n if (!files.length) 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()) return\n\n if (issues.length > 0) {\n emit('error', new FileValidationError(issues, valid))\n return\n }\n\n if (!valid.length) return\n\n // Набор сменился — миниатюры прежнего больше не нужны.\n revokeAllPreviews()\n setFiles(valid)\n\n const before = await runBeforeUpload(valid)\n if (isStale()) 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 } 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 } catch (error) {\n if (error instanceof GrUploadAbortError) {\n setStateIdle()\n } else {\n setStateError(error)\n }\n emit('error', error)\n } finally {\n if (activeController === controller) 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) event.preventDefault()\n}\n\nasync function onInputChange(event: Event) {\n if (isLocked.value) 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) 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) return\n // `openDialog` кликает по input программно, и этот клик всплывает обратно сюда.\n // Без отсечки зона открывала бы диалог по кругу.\n if (event.target === inputRef.value) return\n\n openDialog()\n}\n\nconst effectiveProgressTone = computed<GrProgressBarTone>(() => {\n if (state.phase === 'error') return 'danger'\n if (state.phase === 'success') return 'success'\n return props.progressTone\n})\n\n/**\n * Живой регион существует с первого рендера и пуст, пока объявлять нечего:\n * регион, появляющийся сразу с текстом, часть AT не объявляет вовсе. Прогресс\n * в процентах сюда не идёт — диктор захлебнётся; объявляются только фазы.\n */\nconst liveMessage = computed(() => {\n if (state.phase === 'uploading') return t('gr.fileUpload.uploading', 'Uploading…')\n if (state.phase === 'success') return t('gr.fileUpload.success', 'Upload complete')\n if (state.phase === 'error') 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) 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) 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) 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) assignState(summarizeFileEntries(fileEntries.value))\n else if (state.phase !== 'idle') 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 (process.env.NODE_ENV === 'production') return\n if (props.action || props.request) 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>","<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\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) return true\n\n for (const node of meaningful) {\n if (node.type !== Text) 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) return\n if (next === lastFiles.value) 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\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) 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') return undefined\n if (!Number.isFinite(limit)) return undefined\n if (limit <= 0) return undefined\n return Math.floor(limit)\n}\n\n\nasync function uploadViaAction(files: File[], signal: AbortSignal, extraData: GrFileUploadExtraData | undefined) {\n if (!props.action) 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) 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 } 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) return\n if (!files.length) 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()) return\n\n if (issues.length > 0) {\n emit('error', new FileValidationError(issues, valid))\n return\n }\n\n if (!valid.length) return\n\n // Набор сменился — миниатюры прежнего больше не нужны.\n revokeAllPreviews()\n setFiles(valid)\n\n const before = await runBeforeUpload(valid)\n if (isStale()) 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 } 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 } catch (error) {\n if (error instanceof GrUploadAbortError) {\n setStateIdle()\n } else {\n setStateError(error)\n }\n emit('error', error)\n } finally {\n if (activeController === controller) 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) event.preventDefault()\n}\n\nasync function onInputChange(event: Event) {\n if (isLocked.value) 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) 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) return\n // `openDialog` кликает по input программно, и этот клик всплывает обратно сюда.\n // Без отсечки зона открывала бы диалог по кругу.\n if (event.target === inputRef.value) return\n\n openDialog()\n}\n\nconst effectiveProgressTone = computed<GrProgressBarTone>(() => {\n if (state.phase === 'error') return 'danger'\n if (state.phase === 'success') return 'success'\n return props.progressTone\n})\n\n/**\n * Живой регион существует с первого рендера и пуст, пока объявлять нечего:\n * регион, появляющийся сразу с текстом, часть AT не объявляет вовсе. Прогресс\n * в процентах сюда не идёт — диктор захлебнётся; объявляются только фазы.\n */\nconst liveMessage = computed(() => {\n if (state.phase === 'uploading') return t('gr.fileUpload.uploading', 'Uploading…')\n if (state.phase === 'success') return t('gr.fileUpload.success', 'Upload complete')\n if (state.phase === 'error') 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) 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) 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) 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) assignState(summarizeFileEntries(fileEntries.value))\n else if (state.phase !== 'idle') 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 (process.env.NODE_ENV === 'production') return\n if (props.action || props.request) 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>"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrFormField-kqfVY30N.js","names":[],"sources":["../../../../src/components/GrFormField/GrFormField.vue","../../../../src/components/GrFormField/GrFormField.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, useId, useSlots } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormContext } from '../GrForm/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_FIELD_KEY } from './context'\nimport {\n controlColumnClass,\n errorBaseClass,\n errorTexts,\n fieldGaps,\n hintBaseClass,\n hintTexts,\n labelBaseClass,\n labelInlineClass,\n labelTexts,\n requiredMarkClass,\n rootColumnClass,\n rootRowClass,\n type GrFormFieldLabelPosition,\n type GrFormFieldSize,\n} from './grFormFieldStyles'\n\nexport interface GrFormFieldProps {\n label?: string\n /**\n * Имя поля в модели `GrForm` (в т.ч. dot-path `address.city`). Когда поле внутри\n * `GrForm` и задан `name`, ошибка и признак обязательности берутся из формы по\n * этому имени, а потеря фокуса триггерит валидацию поля.\n */\n name?: string\n /** Явный id контрола. Если не задан — генерируется автоматически. */\n forId?: string\n /**\n * Явная ошибка (или несколько). Перекрывает ошибку из `GrForm` — ручной\n * режим без формы тоже здесь. Массив нужен там, где источник ошибок один,\n * а претензий несколько: ответ сервера, валидация файла.\n */\n error?: string | string[]\n /** Подсказка под лейблом/над контролом (можно также через слот `#hint`). */\n hint?: string\n /** Помечает поле обязательным (маркер `*` + `aria-required` у контрола). */\n required?: boolean\n /** Всё поле только для чтения: контролы внутри перестают редактироваться. */\n readonly?: boolean\n /** Поле недоступно. Складывается с `disabled` формы по «или». */\n disabled?: boolean\n /**\n * Показывать текст ошибки. `false` — поле остаётся невалидным для контрола\n * и AT (`aria-invalid`), но сообщение не занимает места: так делают в\n * плотных таблицах-формах, где ошибка объясняется сводкой сверху.\n */\n showMessage?: boolean\n /** Размер подписи, подсказки и ошибки. Не задан — из `GrConfigProvider`, иначе `md`. */\n size?: GrFormFieldSize\n /** Подпись сверху (по умолчанию) или сбоку — для плотных форм. */\n labelPosition?: GrFormFieldLabelPosition\n /** Ширина колонки подписи при `labelPosition=\"start\"`. Число — пиксели. */\n labelWidth?: string | number\n labelClass?: LabelClass\n}\n\nimport './defaults'\n\ntype LabelClass = string | string[] | Record<string, boolean>\n\nconst props = withDefaults(\n defineProps<GrFormFieldProps>(),\n {\n label: undefined,\n name: undefined,\n forId: undefined,\n error: undefined,\n hint: undefined,\n required: false,\n readonly: false,\n disabled: false,\n showMessage: true,\n size: undefined,\n labelPosition: undefined,\n labelWidth: undefined,\n labelClass: undefined,\n },\n)\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFormField' })\nconst labelPosition = useGrComponentProp('GrFormField', 'labelPosition', () => props.labelPosition, 'top')\nconst isInlineLabel = computed(() => labelPosition.value === 'start')\n\nconst rootClass = computed(() => [\n isInlineLabel.value ? rootRowClass : rootColumnClass,\n fieldGaps[resolvedSize.value],\n])\n\n// Подпись сбоку не сжимается и держит заданную ширину: иначе колонки контролов\n// в форме разъезжаются по длине подписей.\nconst labelStyle = computed(() => {\n if (!isInlineLabel.value || props.labelWidth === undefined)\n return undefined\n\n return { width: typeof props.labelWidth === 'number' ? `${props.labelWidth}px` : props.labelWidth }\n})\n\nconst labelClassName = computed(() => [\n labelBaseClass,\n labelTexts[resolvedSize.value],\n isInlineLabel.value ? labelInlineClass : '',\n])\nconst hintClassName = computed(() => [hintBaseClass, hintTexts[resolvedSize.value]])\nconst errorClassName = computed(() => [errorBaseClass, errorTexts[resolvedSize.value]])\n\n// Контекст `GrForm` (если поле внутри формы). Контролы про форму не знают —\n// оркестрация подключается здесь, на уровне поля.\nconst form = useGrFormContext()\nconst { t } = useGranularityTranslations()\nconst boundToForm = computed(() => Boolean(form && props.name && form.hasField(props.name)))\n\n// Автогенерация id: контрол внутри читает его через inject и связывается с\n// лейблом (`for`) без ручного дублирования `forId`.\nconst generatedId = useId()\nconst fieldId = computed(() => props.forId ?? generatedId)\n\nconst errorId = useId()\nconst hintId = useId()\n\n// Ошибки: явный проп `error` имеет приоритет, иначе — из формы по `name`.\n// Форма отдаёт одну строку, проп — строку или массив; внутри всегда список.\nconst resolvedErrors = computed<string[]>(() => {\n if (props.error)\n return (Array.isArray(props.error) ? props.error : [props.error]).filter(Boolean)\n\n if (boundToForm.value && props.name) {\n const fromForm = form!.errors.value[props.name]\n return fromForm ? [fromForm] : []\n }\n\n return []\n})\n\n// Обязательность: проп `required` ИЛИ наличие `required`-правила в форме.\nconst isRequired = computed(() => {\n if (props.required) return true\n if (boundToForm.value && props.name) return form!.requiredFields.value.has(props.name)\n return false\n})\n\n/** Идёт асинхронная проверка этого поля (правило ходит на сервер). */\nconst isValidating = computed(() =>\n Boolean(boundToForm.value && props.name && form?.validatingFields?.value.has(props.name)),\n)\n\nconst validatingText = computed(() => t('gr.form.validating', 'Checking…'))\n\nconst hasLabel = computed(() => Boolean(props.label) || Boolean(slots.label))\nconst hasHint = computed(() => Boolean(props.hint) || Boolean(slots.hint))\nconst hasError = computed(() => resolvedErrors.value.length > 0)\n// Текст можно скрыть, но невалидность поля от этого не исчезает.\nconst showsMessage = computed(() => hasError.value && props.showMessage)\n// Пока идёт проверка, показываем её вместо старой ошибки: ошибка относится к\n// прежнему значению, и оставлять её на экране — врать про текущее.\nconst showsValidating = computed(() => isValidating.value && props.showMessage)\n\n/**\n * `aria-describedby` контрола: подсказка и ошибка.\n *\n * Id ошибки стоит здесь всегда, а не только когда ошибка есть: часть AT не\n * перечитывает описание после смены самого атрибута. Контейнер ошибки при этом\n * тоже живёт в DOM постоянно и меняет только текст — пустой он ничего не\n * добавляет к описанию.\n */\nconst describedById = computed(() => {\n const ids = [hasHint.value ? hintId : null, errorId].filter(Boolean)\n return ids.length ? ids.join(' ') : undefined\n})\n\nconst labelElementId = useId()\nconst labelId = computed(() => (hasLabel.value ? labelElementId : undefined))\n\nprovide(GR_FORM_FIELD_KEY, {\n id: fieldId,\n labelId,\n readonly: computed(() => props.readonly),\n // «Или», как и у контролов: форма вправе выключить поле, у которого своего\n // `disabled` нет.\n disabled: computed(() => props.disabled || Boolean(form?.disabled?.value)),\n describedById,\n invalid: hasError,\n required: isRequired,\n})\n\n// ————— Интеграция с формой: регистрация (scroll-to-error) + валидация по blur.\nconst fieldRootEl = ref<HTMLElement | null>(null)\nlet unregister: (() => void) | undefined\n\nonMounted(async () => {\n if (form && props.name) {\n unregister = form.registerField(props.name, () => fieldRootEl.value, {\n // Обязательность поля обязана дойти до валидации: иначе звёздочка есть,\n // а submit проходит с пустым полем.\n required: () => props.required,\n })\n }\n\n await nextTick()\n warnIfControlMissed()\n})\n\n/**\n * Контракт поля держится на том, что контрол прочитал `useGrFormFieldContext()`\n * и повесил `id` на себя. Не прочитал — `<label for>` указывает в пустоту:\n * клик по подписи ничего не фокусирует, а связи для скринридера нет, и узнать\n * об этом можно только вручную. Дешёвая страховка — предупреждение в dev.\n */\nfunction warnIfControlMissed(): void {\n if (process.env.NODE_ENV === 'production') return\n if (!hasLabel.value) return\n\n const root = fieldRootEl.value\n if (!root) return\n\n const target = Array.from(root.querySelectorAll<HTMLElement>('[id]'))\n .some(element => element.id === fieldId.value)\n\n if (target) return\n\n console.warn(\n `[GrFormField] Подпись «${props.label ?? ''}» ссылается на id \"${fieldId.value}\", `\n + 'которого нет внутри поля. Контрол должен прочитать `useGrFormFieldContext()` '\n + 'и повесить `field.id` на себя — либо задайте `forId` и тот же `id` на контроле.',\n )\n}\nonBeforeUnmount(() => unregister?.())\n\n// `focusout` всплывает от любого фокусируемого контрола внутри поля (native и\n// кастомного) — единый способ поймать blur без правок самих контролов.\n//\n// Но всплывает он и когда фокус переезжает ВНУТРИ поля: с input на его же\n// кнопку очистки, между чекбоксами группы. Такое поле валидировалось бы раньше,\n// чем пользователь его дозаполнил, поэтому blur считается состоявшимся только\n// если фокус ушёл за пределы корня. `relatedTarget: null` (фокус в никуда:\n// клик по body, уход из окна) — тоже уход.\nfunction onFocusOut(event: FocusEvent): void {\n if (!form || !props.name) return\n\n const next = event.relatedTarget as Node | null\n if (next && fieldRootEl.value?.contains(next)) return\n\n void form.validateField(props.name, 'blur')\n}\n</script>\n\n<template>\n <div\n ref=\"fieldRootEl\"\n data-gr-form-field\n :class=\"rootClass\"\n :aria-busy=\"isValidating ? 'true' : undefined\"\n @focusout=\"onFocusOut\"\n >\n <label\n v-if=\"hasLabel\"\n :id=\"labelId\"\n :for=\"fieldId\"\n :class=\"[labelClassName, props.labelClass]\"\n :style=\"labelStyle\"\n >\n <slot name=\"label\">{{ props.label }}</slot>\n <span v-if=\"isRequired\" data-gr-form-field-required aria-hidden=\"true\" :class=\"requiredMarkClass\">*</span>\n </label>\n\n <!-- Подсказка, контрол и ошибка живут одной колонкой: при подписи сбоку\n они остаются рядом с контролом, а не разъезжаются по строке. -->\n <div data-gr-form-field-control :class=\"[controlColumnClass, fieldGaps[resolvedSize]]\">\n <p\n v-if=\"hasHint\"\n :id=\"hintId\"\n data-gr-form-field-hint\n :class=\"hintClassName\"\n >\n <slot name=\"hint\">\n {{ props.hint }}\n </slot>\n </p>\n\n <slot />\n\n <!-- Пустой контейнер уводится в `sr-only`, чтобы не добавлять пустую\n строку в колонку поля: gap дал бы видимый отступ. -->\n <div\n :id=\"errorId\"\n data-gr-form-field-error\n role=\"alert\"\n :class=\"showsMessage || showsValidating ? errorClassName : 'sr-only'\"\n >\n <span v-if=\"showsValidating\" data-gr-form-field-validating :class=\"hintClassName\">\n {{ validatingText }}\n </span>\n <slot v-else-if=\"showsMessage\" name=\"error\" :errors=\"resolvedErrors\">\n <div\n v-for=\"(message, index) in resolvedErrors\"\n :key=\"index\"\n data-gr-form-field-error-item\n >\n {{ message }}\n </div>\n </slot>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, useId, useSlots } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormContext } from '../GrForm/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_FIELD_KEY } from './context'\nimport {\n controlColumnClass,\n errorBaseClass,\n errorTexts,\n fieldGaps,\n hintBaseClass,\n hintTexts,\n labelBaseClass,\n labelInlineClass,\n labelTexts,\n requiredMarkClass,\n rootColumnClass,\n rootRowClass,\n type GrFormFieldLabelPosition,\n type GrFormFieldSize,\n} from './grFormFieldStyles'\n\nexport interface GrFormFieldProps {\n label?: string\n /**\n * Имя поля в модели `GrForm` (в т.ч. dot-path `address.city`). Когда поле внутри\n * `GrForm` и задан `name`, ошибка и признак обязательности берутся из формы по\n * этому имени, а потеря фокуса триггерит валидацию поля.\n */\n name?: string\n /** Явный id контрола. Если не задан — генерируется автоматически. */\n forId?: string\n /**\n * Явная ошибка (или несколько). Перекрывает ошибку из `GrForm` — ручной\n * режим без формы тоже здесь. Массив нужен там, где источник ошибок один,\n * а претензий несколько: ответ сервера, валидация файла.\n */\n error?: string | string[]\n /** Подсказка под лейблом/над контролом (можно также через слот `#hint`). */\n hint?: string\n /** Помечает поле обязательным (маркер `*` + `aria-required` у контрола). */\n required?: boolean\n /** Всё поле только для чтения: контролы внутри перестают редактироваться. */\n readonly?: boolean\n /** Поле недоступно. Складывается с `disabled` формы по «или». */\n disabled?: boolean\n /**\n * Показывать текст ошибки. `false` — поле остаётся невалидным для контрола\n * и AT (`aria-invalid`), но сообщение не занимает места: так делают в\n * плотных таблицах-формах, где ошибка объясняется сводкой сверху.\n */\n showMessage?: boolean\n /** Размер подписи, подсказки и ошибки. Не задан — из `GrConfigProvider`, иначе `md`. */\n size?: GrFormFieldSize\n /** Подпись сверху (по умолчанию) или сбоку — для плотных форм. */\n labelPosition?: GrFormFieldLabelPosition\n /** Ширина колонки подписи при `labelPosition=\"start\"`. Число — пиксели. */\n labelWidth?: string | number\n labelClass?: LabelClass\n}\n\nimport './defaults'\n\ntype LabelClass = string | string[] | Record<string, boolean>\n\nconst props = withDefaults(\n defineProps<GrFormFieldProps>(),\n {\n label: undefined,\n name: undefined,\n forId: undefined,\n error: undefined,\n hint: undefined,\n required: false,\n readonly: false,\n disabled: false,\n showMessage: true,\n size: undefined,\n labelPosition: undefined,\n labelWidth: undefined,\n labelClass: undefined,\n },\n)\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFormField' })\nconst labelPosition = useGrComponentProp('GrFormField', 'labelPosition', () => props.labelPosition, 'top')\nconst isInlineLabel = computed(() => labelPosition.value === 'start')\n\nconst rootClass = computed(() => [\n isInlineLabel.value ? rootRowClass : rootColumnClass,\n fieldGaps[resolvedSize.value],\n])\n\n// Подпись сбоку не сжимается и держит заданную ширину: иначе колонки контролов\n// в форме разъезжаются по длине подписей.\nconst labelStyle = computed(() => {\n if (!isInlineLabel.value || props.labelWidth === undefined)\n return undefined\n\n return { width: typeof props.labelWidth === 'number' ? `${props.labelWidth}px` : props.labelWidth }\n})\n\nconst labelClassName = computed(() => [\n labelBaseClass,\n labelTexts[resolvedSize.value],\n isInlineLabel.value ? labelInlineClass : '',\n])\nconst hintClassName = computed(() => [hintBaseClass, hintTexts[resolvedSize.value]])\nconst errorClassName = computed(() => [errorBaseClass, errorTexts[resolvedSize.value]])\n\n// Контекст `GrForm` (если поле внутри формы). Контролы про форму не знают —\n// оркестрация подключается здесь, на уровне поля.\nconst form = useGrFormContext()\nconst { t } = useGranularityTranslations()\nconst boundToForm = computed(() => Boolean(form && props.name && form.hasField(props.name)))\n\n// Автогенерация id: контрол внутри читает его через inject и связывается с\n// лейблом (`for`) без ручного дублирования `forId`.\nconst generatedId = useId()\nconst fieldId = computed(() => props.forId ?? generatedId)\n\nconst errorId = useId()\nconst hintId = useId()\n\n// Ошибки: явный проп `error` имеет приоритет, иначе — из формы по `name`.\n// Форма отдаёт одну строку, проп — строку или массив; внутри всегда список.\nconst resolvedErrors = computed<string[]>(() => {\n if (props.error)\n return (Array.isArray(props.error) ? props.error : [props.error]).filter(Boolean)\n\n if (boundToForm.value && props.name) {\n const fromForm = form!.errors.value[props.name]\n return fromForm ? [fromForm] : []\n }\n\n return []\n})\n\n// Обязательность: проп `required` ИЛИ наличие `required`-правила в форме.\nconst isRequired = computed(() => {\n if (props.required) return true\n if (boundToForm.value && props.name) return form!.requiredFields.value.has(props.name)\n return false\n})\n\n/** Идёт асинхронная проверка этого поля (правило ходит на сервер). */\nconst isValidating = computed(() =>\n Boolean(boundToForm.value && props.name && form?.validatingFields?.value.has(props.name)),\n)\n\nconst validatingText = computed(() => t('gr.form.validating', 'Checking…'))\n\nconst hasLabel = computed(() => Boolean(props.label) || Boolean(slots.label))\nconst hasHint = computed(() => Boolean(props.hint) || Boolean(slots.hint))\nconst hasError = computed(() => resolvedErrors.value.length > 0)\n// Текст можно скрыть, но невалидность поля от этого не исчезает.\nconst showsMessage = computed(() => hasError.value && props.showMessage)\n// Пока идёт проверка, показываем её вместо старой ошибки: ошибка относится к\n// прежнему значению, и оставлять её на экране — врать про текущее.\nconst showsValidating = computed(() => isValidating.value && props.showMessage)\n\n/**\n * `aria-describedby` контрола: подсказка и ошибка.\n *\n * Id ошибки стоит здесь всегда, а не только когда ошибка есть: часть AT не\n * перечитывает описание после смены самого атрибута. Контейнер ошибки при этом\n * тоже живёт в DOM постоянно и меняет только текст — пустой он ничего не\n * добавляет к описанию.\n */\nconst describedById = computed(() => {\n const ids = [hasHint.value ? hintId : null, errorId].filter(Boolean)\n return ids.length ? ids.join(' ') : undefined\n})\n\nconst labelElementId = useId()\nconst labelId = computed(() => (hasLabel.value ? labelElementId : undefined))\n\nprovide(GR_FORM_FIELD_KEY, {\n id: fieldId,\n labelId,\n readonly: computed(() => props.readonly),\n // «Или», как и у контролов: форма вправе выключить поле, у которого своего\n // `disabled` нет.\n disabled: computed(() => props.disabled || Boolean(form?.disabled?.value)),\n describedById,\n invalid: hasError,\n required: isRequired,\n})\n\n// ————— Интеграция с формой: регистрация (scroll-to-error) + валидация по blur.\nconst fieldRootEl = ref<HTMLElement | null>(null)\nlet unregister: (() => void) | undefined\n\nonMounted(async () => {\n if (form && props.name) {\n unregister = form.registerField(props.name, () => fieldRootEl.value, {\n // Обязательность поля обязана дойти до валидации: иначе звёздочка есть,\n // а submit проходит с пустым полем.\n required: () => props.required,\n })\n }\n\n await nextTick()\n warnIfControlMissed()\n})\n\n/**\n * Контракт поля держится на том, что контрол прочитал `useGrFormFieldContext()`\n * и повесил `id` на себя. Не прочитал — `<label for>` указывает в пустоту:\n * клик по подписи ничего не фокусирует, а связи для скринридера нет, и узнать\n * об этом можно только вручную. Дешёвая страховка — предупреждение в dev.\n */\nfunction warnIfControlMissed(): void {\n if (process.env.NODE_ENV === 'production') return\n if (!hasLabel.value) return\n\n const root = fieldRootEl.value\n if (!root) return\n\n const target = Array.from(root.querySelectorAll<HTMLElement>('[id]'))\n .some(element => element.id === fieldId.value)\n\n if (target) return\n\n console.warn(\n `[GrFormField] Подпись «${props.label ?? ''}» ссылается на id \"${fieldId.value}\", `\n + 'которого нет внутри поля. Контрол должен прочитать `useGrFormFieldContext()` '\n + 'и повесить `field.id` на себя — либо задайте `forId` и тот же `id` на контроле.',\n )\n}\nonBeforeUnmount(() => unregister?.())\n\n// `focusout` всплывает от любого фокусируемого контрола внутри поля (native и\n// кастомного) — единый способ поймать blur без правок самих контролов.\n//\n// Но всплывает он и когда фокус переезжает ВНУТРИ поля: с input на его же\n// кнопку очистки, между чекбоксами группы. Такое поле валидировалось бы раньше,\n// чем пользователь его дозаполнил, поэтому blur считается состоявшимся только\n// если фокус ушёл за пределы корня. `relatedTarget: null` (фокус в никуда:\n// клик по body, уход из окна) — тоже уход.\nfunction onFocusOut(event: FocusEvent): void {\n if (!form || !props.name) return\n\n const next = event.relatedTarget as Node | null\n if (next && fieldRootEl.value?.contains(next)) return\n\n void form.validateField(props.name, 'blur')\n}\n</script>\n\n<template>\n <div\n ref=\"fieldRootEl\"\n data-gr-form-field\n :class=\"rootClass\"\n :aria-busy=\"isValidating ? 'true' : undefined\"\n @focusout=\"onFocusOut\"\n >\n <label\n v-if=\"hasLabel\"\n :id=\"labelId\"\n :for=\"fieldId\"\n :class=\"[labelClassName, props.labelClass]\"\n :style=\"labelStyle\"\n >\n <slot name=\"label\">{{ props.label }}</slot>\n <span v-if=\"isRequired\" data-gr-form-field-required aria-hidden=\"true\" :class=\"requiredMarkClass\">*</span>\n </label>\n\n <!-- Подсказка, контрол и ошибка живут одной колонкой: при подписи сбоку\n они остаются рядом с контролом, а не разъезжаются по строке. -->\n <div data-gr-form-field-control :class=\"[controlColumnClass, fieldGaps[resolvedSize]]\">\n <p\n v-if=\"hasHint\"\n :id=\"hintId\"\n data-gr-form-field-hint\n :class=\"hintClassName\"\n >\n <slot name=\"hint\">\n {{ props.hint }}\n </slot>\n </p>\n\n <slot />\n\n <!-- Пустой контейнер уводится в `sr-only`, чтобы не добавлять пустую\n строку в колонку поля: gap дал бы видимый отступ. -->\n <div\n :id=\"errorId\"\n data-gr-form-field-error\n role=\"alert\"\n :class=\"showsMessage || showsValidating ? errorClassName : 'sr-only'\"\n >\n <span v-if=\"showsValidating\" data-gr-form-field-validating :class=\"hintClassName\">\n {{ validatingText }}\n </span>\n <slot v-else-if=\"showsMessage\" name=\"error\" :errors=\"resolvedErrors\">\n <div\n v-for=\"(message, index) in resolvedErrors\"\n :key=\"index\"\n data-gr-form-field-error-item\n >\n {{ message }}\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":"GrJsonViewer-DFUppRNb.js","names":[],"sources":["../../../../src/components/GrJsonViewer/jsonToNodes.ts","../../../../src/components/GrJsonViewer/GrJsonViewer.vue","../../../../src/components/GrJsonViewer/GrJsonViewer.vue"],"sourcesContent":["/**\n * Разбор произвольного значения в узлы дерева.\n *\n * Компонент показывает данные из БД и от чужих сервисов, поэтому обход обязан\n * пережить всё: цикл, `BigInt`, функцию в поле, массив на десять тысяч записей,\n * строку в миллион символов. Уронить страницу он права не имеет.\n */\n\n/** Что за значение в узле. Решает и цвет, и то, раскрывается ли узел. */\nexport type GrJsonNodeKind =\n | 'object'\n | 'array'\n | 'string'\n | 'number'\n | 'boolean'\n | 'null'\n /** Значения, которых в JSON не бывает: функция, символ, `undefined`. */\n | 'unsupported'\n /** Хвост обрезанного массива — «ещё N элементов». Своего значения не имеет. */\n | 'truncation'\n\nexport type GrJsonNode = {\n /** Читаемый адрес узла (`$.items[3].name`). Он же ключ для `GrTree`. */\n path: string\n /** Имя ключа или индекс. Показывается слева от двоеточия. */\n label: string\n kind: GrJsonNodeKind\n /** Что видно в строке. У ветки — счётчик, у листа — значение, возможно обрезанное. */\n preview: string\n /** Полное значение. Копирование берёт его, а не `preview`. */\n value: unknown\n /** Показ обрезан: копирование всё равно отдаёт целое. */\n truncated: boolean\n isLeaf: boolean\n children?: GrJsonNode[]\n}\n\nexport type GrJsonToNodesOptions = {\n /** Подпись корня. */\n rootLabel?: string\n /** Длина строки, после которой значение обрезается в показе. */\n maxStringLength?: number\n /** Сколько элементов массива разбирать до заглушки «ещё N». */\n maxArrayItems?: number\n}\n\n/** Маркер ветки, которая уже встречалась выше по этой же цепочке предков. */\nexport const CIRCULAR_MARKER = '[Circular]'\n\nconst DEFAULT_ROOT_LABEL = '$'\nconst DEFAULT_MAX_STRING_LENGTH = 200\nconst DEFAULT_MAX_ARRAY_ITEMS = 100\n\n/** Ключ, который можно дописать через точку, не сломав адрес. */\nconst PLAIN_KEY = /^[A-Za-z_$][\\w$]*$/\n\nfunction appendKey(parentPath: string, key: string): string {\n return PLAIN_KEY.test(key) ? `${parentPath}.${key}` : `${parentPath}[${JSON.stringify(key)}]`\n}\n\n/**\n * Счётчик у ветки вместо её содержимого: свёрнутый узел обязан сказать, стоит\n * ли его раскрывать. Слово не склоняется — компонент не знает языка потребителя,\n * а число само по себе читается в любом.\n */\nfunction branchPreview(kind: 'object' | 'array', count: number): string {\n return kind === 'array' ? `[${count}]` : `{${count}}`\n}\n\nfunction scalarPreview(value: unknown, maxStringLength: number): { preview: string, truncated: boolean } {\n if (typeof value === 'string') {\n if (value.length <= maxStringLength) return { preview: JSON.stringify(value), truncated: false }\n\n // Кавычка открывается и не закрывается намеренно: закрытая читалась бы как\n // конец строки, а строка продолжается — её просто не показывают целиком.\n return { preview: `${JSON.stringify(value.slice(0, maxStringLength))}…`, truncated: true }\n }\n\n // `BigInt` представления в JSON не имеет, и `JSON.stringify` на нём бросает.\n if (typeof value === 'bigint') return { preview: `${value}n`, truncated: false }\n\n if (typeof value === 'function' || typeof value === 'symbol' || value === undefined)\n return { preview: String(value), truncated: false }\n\n return { preview: JSON.stringify(value) ?? String(value), truncated: false }\n}\n\nfunction kindOf(value: unknown): GrJsonNodeKind {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'array'\n\n switch (typeof value) {\n case 'string': return 'string'\n case 'number': return 'number'\n case 'bigint': return 'number'\n case 'boolean': return 'boolean'\n case 'object': return 'object'\n // `undefined`, функция и символ представления в JSON не имеют — узел\n // остаётся, чтобы ключ не исчез из показа молча.\n case 'undefined':\n case 'function':\n case 'symbol': return 'unsupported'\n }\n\n // Недостижимо: ветки выше покрывают `typeof` целиком. Оставлено ради вывода\n // типов — без него TS не считает функцию всегда возвращающей.\n return 'unsupported'\n}\n\ntype BuildContext = {\n maxStringLength: number\n maxArrayItems: number\n /**\n * Цепочка предков текущего узла, а не все встреченные объекты.\n *\n * Разница принципиальна и отличает обход дерева от `replacer` у\n * `JSON.stringify`: тому стек недоступен, поэтому он метит `[Circular]` любую\n * повторную ссылку — в том числе объект, честно положенный в данные дважды.\n * Здесь стек есть, и маркер достаётся только настоящему циклу.\n */\n ancestors: Set<object>\n}\n\nfunction buildNode(label: string, path: string, value: unknown, context: BuildContext): GrJsonNode {\n const kind = kindOf(value)\n\n if (kind !== 'object' && kind !== 'array') {\n const { preview, truncated } = scalarPreview(value, context.maxStringLength)\n\n return { path, label, kind, preview, value, truncated, isLeaf: true }\n }\n\n const container = value as object\n\n if (context.ancestors.has(container)) {\n return { path, label, kind, preview: CIRCULAR_MARKER, value, truncated: false, isLeaf: true }\n }\n\n const entries = Array.isArray(value)\n ? value.map((item, index) => [String(index), item] as const)\n : Object.entries(container)\n\n // Пустой объект и пустой массив — листья: раскрывать нечего, а ветка без\n // детей рисует стрелку, которая ничего не делает.\n if (entries.length === 0) {\n return {\n path,\n label,\n kind,\n preview: Array.isArray(value) ? '[]' : '{}',\n value,\n truncated: false,\n isLeaf: true,\n }\n }\n\n context.ancestors.add(container)\n\n const limit = Math.max(0, context.maxArrayItems)\n const isOverLimit = Array.isArray(value) && entries.length > limit\n const visible = isOverLimit ? entries.slice(0, limit) : entries\n\n const children: GrJsonNode[] = visible.map(([key, item]) => buildNode(\n key,\n Array.isArray(value) ? `${path}[${key}]` : appendKey(path, key),\n item,\n context,\n ))\n\n if (isOverLimit) {\n const rest = entries.length - limit\n\n children.push({\n path: `${path}[…]`,\n label: '…',\n kind: 'truncation',\n preview: String(rest),\n value: (value as unknown[]).slice(limit),\n truncated: true,\n isLeaf: true,\n })\n }\n\n context.ancestors.delete(container)\n\n return {\n path,\n label,\n kind,\n preview: branchPreview(kind, entries.length),\n value,\n truncated: isOverLimit,\n isLeaf: false,\n children,\n }\n}\n\n/**\n * Значение → узлы дерева. Корень всегда один: даже скаляр на входе — это узел,\n * иначе дерево пришлось бы то показывать, то нет.\n */\nexport function jsonToNodes(value: unknown, options: GrJsonToNodesOptions = {}): GrJsonNode[] {\n const context: BuildContext = {\n maxStringLength: options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH,\n maxArrayItems: options.maxArrayItems ?? DEFAULT_MAX_ARRAY_ITEMS,\n ancestors: new Set(),\n }\n\n return [buildNode(options.rootLabel ?? DEFAULT_ROOT_LABEL, DEFAULT_ROOT_LABEL, value, context)]\n}\n\n/** Пути всех узлов до заданной глубины — ими задаётся начальная свёртка. */\nexport function pathsToDepth(nodes: GrJsonNode[], depth: number, level = 0): string[] {\n if (level >= depth) return []\n\n return nodes.flatMap(node => (\n node.children ? [node.path, ...pathsToDepth(node.children, depth, level + 1)] : []\n ))\n}\n\n/** Пути всех веток — для «раскрыть всё». */\nexport function branchPaths(nodes: GrJsonNode[]): string[] {\n return nodes.flatMap(node => (node.children ? [node.path, ...branchPaths(node.children)] : []))\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrTree from '../GrTree/GrTree.vue'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\n\nimport IconCheck from '~icons/lucide/check'\nimport IconCopy from '~icons/lucide/copy'\n\nimport { branchPaths, jsonToNodes, pathsToDepth, type GrJsonNode } from './jsonToNodes'\nimport {\n jsonValueClass,\n jsonViewerContentClass,\n jsonViewerCopyClass,\n jsonViewerGroupClass,\n jsonViewerKeyClass,\n jsonViewerMutedClass,\n jsonViewerPunctuationClass,\n jsonViewerRootClass,\n jsonViewerRowClass,\n jsonViewerSearchClass,\n jsonViewerToolbarClass,\n jsonViewerValueClass,\n} from './grJsonViewerStyles'\n\nimport type { GrComponentSize } from '../shared/sizes'\n\nexport type { GrJsonNode, GrJsonNodeKind } from './jsonToNodes'\n\n/**\n * JSON со свёрткой по узлам, поиском по ключу и значению и копированием узла.\n *\n * Дерево, клавиатуру и виртуализацию несёт `GrTree`; здесь — разбор `unknown` в\n * узлы и разметка строки. Плоский текст целиком, который копируют в тикет, —\n * `GrCodeBlock`.\n */\nexport interface GrJsonViewerProps {\n /** Показываемое значение. `unknown`, потому что приходит из БД или чужого сервиса. */\n value: unknown\n /** Подпись корня. По умолчанию `$` — так адресуют путь JSONPath и dev-tools. */\n rootLabel?: string\n /** Сколько уровней раскрыто изначально. */\n defaultExpandDepth?: number\n /**\n * Длина строкового значения, после которой показ обрезается.\n *\n * Не косметика: запрос к модели с картинкой в base64 — это один лист на\n * сотни тысяч символов, и ни свёртка, ни виртуализация по строкам его не\n * берут. Копирование при этом отдаёт значение целиком.\n */\n maxStringLength?: number\n /** Сколько элементов массива разбирать до заглушки «ещё N». */\n maxArrayItems?: number\n /** Поле поиска над деревом. Выключено — поиск остаётся на `filter()` из `defineExpose`. */\n searchable?: boolean\n /** Кнопка копирования узла в строке. */\n copyable?: boolean\n /** Оставлять в DOM только окно вокруг вьюпорта. Требует `maxHeight`. */\n virtual?: boolean\n /** Высота области просмотра: число — пиксели, строка — как есть. */\n maxHeight?: string | number\n /** Имя области для скринридера. Безымянное дерево объявляется просто «дерево». */\n ariaLabel?: string\n size?: GrComponentSize\n}\n\nexport interface GrJsonViewerEmits {\n /** Узел скопирован: путь для тикета и полное значение. */\n (e: 'copy', payload: { path: string, value: unknown }): void\n}\n\nconst props = withDefaults(defineProps<GrJsonViewerProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n rootLabel: undefined,\n defaultExpandDepth: undefined,\n maxStringLength: undefined,\n maxArrayItems: undefined,\n searchable: undefined,\n copyable: undefined,\n virtual: false,\n maxHeight: undefined,\n ariaLabel: undefined,\n size: undefined,\n})\n\nconst emit = defineEmits<GrJsonViewerEmits>()\n\ndefineSlots<{\n /** Значение листа: ссылка, дата, денежная сумма — оформление знает приложение. */\n leaf?: (props: { node: GrJsonNode }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\nconst { announce } = useAnnouncer()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrJsonViewer' })\nconst resolvedDepth = useGrComponentProp('GrJsonViewer', 'defaultExpandDepth', () => props.defaultExpandDepth, 1)\nconst resolvedMaxString = useGrComponentProp('GrJsonViewer', 'maxStringLength', () => props.maxStringLength, 200)\nconst resolvedMaxArray = useGrComponentProp('GrJsonViewer', 'maxArrayItems', () => props.maxArrayItems, 100)\nconst resolvedSearchable = useGrComponentProp('GrJsonViewer', 'searchable', () => props.searchable, true)\nconst resolvedCopyable = useGrComponentProp('GrJsonViewer', 'copyable', () => props.copyable, true)\n\nconst nodes = computed(() => jsonToNodes(props.value, {\n rootLabel: props.rootLabel,\n maxStringLength: resolvedMaxString.value,\n maxArrayItems: resolvedMaxArray.value,\n}))\n\n/**\n * Раскрытие ведётся подстановкой набора путей.\n *\n * Контролируемого `expandedKeys` у `GrTree` нет, но `defaultExpandedKeys` —\n * не «начальное значение»: дерево следит за пропом и заменяет набор целиком.\n * Этого хватает и глубине по умолчанию, и кнопкам «раскрыть/свернуть всё»;\n * ручное раскрытие пользователя при этом сбрасывается — от этих кнопок ровно\n * того и ждут.\n */\nconst expandedPaths = ref<string[]>([])\n\nwatch(\n [nodes, resolvedDepth],\n ([list, depth]) => { expandedPaths.value = pathsToDepth(list, depth) },\n { immediate: true },\n)\n\nfunction expandAll(): void {\n expandedPaths.value = branchPaths(nodes.value)\n}\n\nfunction collapseAll(): void {\n expandedPaths.value = []\n}\n\nconst query = ref('')\nconst treeRef = ref<{ filter: (value: string) => void } | null>(null)\n\nwatch(query, (value) => { treeRef.value?.filter(value) })\n\nfunction filter(value: string): void {\n query.value = value\n}\n\n/** Поиск идёт и по ключу, и по значению: в чужом ответе ищут то одно, то другое. */\nfunction matchKeyOrValue(value: string, data: GrJsonNode): boolean {\n if (!value) return true\n\n const needle = value.toLowerCase()\n\n return data.label.toLowerCase().includes(needle) || data.preview.toLowerCase().includes(needle)\n}\n\n/**\n * Наличие буфера уточняется после монтирования: `navigator` в теле `setup` либо\n * роняет серверный рендер, либо расходится с ним. До этого момента кнопки нет —\n * ровно то же, что отдаёт сервер, поэтому гидрация совпадает.\n */\nconst canCopy = ref(false)\n\nonMounted(() => {\n canCopy.value = typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function'\n\n if (props.virtual && props.maxHeight === undefined && typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') {\n console.warn('[GrJsonViewer] `virtual` без `maxHeight` не включает виртуализацию: окно считается от высоты области.')\n }\n})\n\nconst showCopy = computed(() => resolvedCopyable.value && canCopy.value)\n\nconst copiedPath = ref<string | null>(null)\nlet copiedTimer: number | null = null\n\nfunction resetCopiedTimer(): void {\n if (copiedTimer !== null) {\n clearTimeout(copiedTimer)\n copiedTimer = null\n }\n}\n\nonBeforeUnmount(resetCopiedTimer)\n\n/**\n * В буфер уходит **полное** значение узла, а не то, что видно: обрезка\n * принадлежит показу, и вставить её обратно нельзя.\n */\nasync function copyNode(node: GrJsonNode): Promise<void> {\n const text = typeof node.value === 'string' ? node.value : JSON.stringify(node.value, null, 2) ?? String(node.value)\n\n try {\n await navigator.clipboard.writeText(text)\n }\n catch {\n announce(t('gr.jsonViewer.copyFailed', 'Could not copy value'))\n return\n }\n\n copiedPath.value = node.path\n resetCopiedTimer()\n copiedTimer = window.setTimeout(() => {\n copiedPath.value = null\n copiedTimer = null\n }, 1200)\n\n announce(t('gr.jsonViewer.copied', 'Value copied'))\n emit('copy', { path: node.path, value: node.value })\n}\n\ndefineExpose({ filter, expandAll, collapseAll })\n</script>\n\n<template>\n <div data-gr-json-viewer :class=\"jsonViewerRootClass\">\n <div v-if=\"resolvedSearchable\" data-gr-json-viewer-toolbar :class=\"jsonViewerToolbarClass\">\n <GrInput\n v-model=\"query\"\n :class=\"jsonViewerSearchClass\"\n :size=\"resolvedSize\"\n clearable\n :placeholder=\"t('gr.jsonViewer.search', 'Search key or value')\"\n :aria-label=\"t('gr.jsonViewer.search', 'Search key or value')\"\n />\n\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"expandAll\">\n {{ t('gr.jsonViewer.expandAll', 'Expand all') }}\n </GrButton>\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"collapseAll\">\n {{ t('gr.jsonViewer.collapseAll', 'Collapse all') }}\n </GrButton>\n </div>\n\n <GrTree\n ref=\"treeRef\"\n :data=\"nodes\"\n node-key=\"path\"\n :props=\"{ children: 'children', label: 'label', isLeaf: 'isLeaf' }\"\n :default-expanded-keys=\"expandedPaths\"\n :filter-node-method=\"matchKeyOrValue\"\n :content-class=\"jsonViewerContentClass\"\n :virtual=\"virtual\"\n :max-height=\"maxHeight\"\n :size=\"resolvedSize\"\n :aria-label=\"ariaLabel\"\n >\n <template #default=\"{ data }\">\n <span data-gr-json-viewer-row :class=\"[jsonViewerRowClass, jsonViewerGroupClass]\">\n <span data-gr-json-viewer-key :class=\"jsonViewerKeyClass\">{{ data.label }}</span>\n <span :class=\"jsonViewerPunctuationClass\">:</span>\n\n <span\n data-gr-json-viewer-value\n :class=\"[jsonViewerValueClass, jsonValueClass[(data as GrJsonNode).kind]]\"\n >\n <slot name=\"leaf\" :node=\"data as GrJsonNode\">{{ (data as GrJsonNode).preview }}</slot>\n </span>\n\n <span\n v-if=\"(data as GrJsonNode).kind === 'truncation'\"\n :class=\"jsonViewerMutedClass\"\n >{{ t('gr.jsonViewer.moreItems', 'more') }}</span>\n\n <GrButton\n v-if=\"showCopy && (data as GrJsonNode).kind !== 'truncation'\"\n data-gr-json-viewer-copy\n variant=\"ghost\"\n size=\"xs\"\n square\n :class=\"jsonViewerCopyClass\"\n :aria-label=\"t('gr.jsonViewer.copy', 'Copy value')\"\n :title=\"t('gr.jsonViewer.copy', 'Copy value')\"\n @click.stop=\"copyNode(data as GrJsonNode)\"\n >\n <component\n :is=\"copiedPath === (data as GrJsonNode).path ? IconCheck : IconCopy\"\n class=\"h-3.5 w-3.5\"\n aria-hidden=\"true\"\n />\n </GrButton>\n </span>\n </template>\n </GrTree>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrTree from '../GrTree/GrTree.vue'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\n\nimport IconCheck from '~icons/lucide/check'\nimport IconCopy from '~icons/lucide/copy'\n\nimport { branchPaths, jsonToNodes, pathsToDepth, type GrJsonNode } from './jsonToNodes'\nimport {\n jsonValueClass,\n jsonViewerContentClass,\n jsonViewerCopyClass,\n jsonViewerGroupClass,\n jsonViewerKeyClass,\n jsonViewerMutedClass,\n jsonViewerPunctuationClass,\n jsonViewerRootClass,\n jsonViewerRowClass,\n jsonViewerSearchClass,\n jsonViewerToolbarClass,\n jsonViewerValueClass,\n} from './grJsonViewerStyles'\n\nimport type { GrComponentSize } from '../shared/sizes'\n\nexport type { GrJsonNode, GrJsonNodeKind } from './jsonToNodes'\n\n/**\n * JSON со свёрткой по узлам, поиском по ключу и значению и копированием узла.\n *\n * Дерево, клавиатуру и виртуализацию несёт `GrTree`; здесь — разбор `unknown` в\n * узлы и разметка строки. Плоский текст целиком, который копируют в тикет, —\n * `GrCodeBlock`.\n */\nexport interface GrJsonViewerProps {\n /** Показываемое значение. `unknown`, потому что приходит из БД или чужого сервиса. */\n value: unknown\n /** Подпись корня. По умолчанию `$` — так адресуют путь JSONPath и dev-tools. */\n rootLabel?: string\n /** Сколько уровней раскрыто изначально. */\n defaultExpandDepth?: number\n /**\n * Длина строкового значения, после которой показ обрезается.\n *\n * Не косметика: запрос к модели с картинкой в base64 — это один лист на\n * сотни тысяч символов, и ни свёртка, ни виртуализация по строкам его не\n * берут. Копирование при этом отдаёт значение целиком.\n */\n maxStringLength?: number\n /** Сколько элементов массива разбирать до заглушки «ещё N». */\n maxArrayItems?: number\n /** Поле поиска над деревом. Выключено — поиск остаётся на `filter()` из `defineExpose`. */\n searchable?: boolean\n /** Кнопка копирования узла в строке. */\n copyable?: boolean\n /** Оставлять в DOM только окно вокруг вьюпорта. Требует `maxHeight`. */\n virtual?: boolean\n /** Высота области просмотра: число — пиксели, строка — как есть. */\n maxHeight?: string | number\n /** Имя области для скринридера. Безымянное дерево объявляется просто «дерево». */\n ariaLabel?: string\n size?: GrComponentSize\n}\n\nexport interface GrJsonViewerEmits {\n /** Узел скопирован: путь для тикета и полное значение. */\n (e: 'copy', payload: { path: string, value: unknown }): void\n}\n\nconst props = withDefaults(defineProps<GrJsonViewerProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n rootLabel: undefined,\n defaultExpandDepth: undefined,\n maxStringLength: undefined,\n maxArrayItems: undefined,\n searchable: undefined,\n copyable: undefined,\n virtual: false,\n maxHeight: undefined,\n ariaLabel: undefined,\n size: undefined,\n})\n\nconst emit = defineEmits<GrJsonViewerEmits>()\n\ndefineSlots<{\n /** Значение листа: ссылка, дата, денежная сумма — оформление знает приложение. */\n leaf?: (props: { node: GrJsonNode }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\nconst { announce } = useAnnouncer()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrJsonViewer' })\nconst resolvedDepth = useGrComponentProp('GrJsonViewer', 'defaultExpandDepth', () => props.defaultExpandDepth, 1)\nconst resolvedMaxString = useGrComponentProp('GrJsonViewer', 'maxStringLength', () => props.maxStringLength, 200)\nconst resolvedMaxArray = useGrComponentProp('GrJsonViewer', 'maxArrayItems', () => props.maxArrayItems, 100)\nconst resolvedSearchable = useGrComponentProp('GrJsonViewer', 'searchable', () => props.searchable, true)\nconst resolvedCopyable = useGrComponentProp('GrJsonViewer', 'copyable', () => props.copyable, true)\n\nconst nodes = computed(() => jsonToNodes(props.value, {\n rootLabel: props.rootLabel,\n maxStringLength: resolvedMaxString.value,\n maxArrayItems: resolvedMaxArray.value,\n}))\n\n/**\n * Раскрытие ведётся подстановкой набора путей.\n *\n * Контролируемого `expandedKeys` у `GrTree` нет, но `defaultExpandedKeys` —\n * не «начальное значение»: дерево следит за пропом и заменяет набор целиком.\n * Этого хватает и глубине по умолчанию, и кнопкам «раскрыть/свернуть всё»;\n * ручное раскрытие пользователя при этом сбрасывается — от этих кнопок ровно\n * того и ждут.\n */\nconst expandedPaths = ref<string[]>([])\n\nwatch(\n [nodes, resolvedDepth],\n ([list, depth]) => { expandedPaths.value = pathsToDepth(list, depth) },\n { immediate: true },\n)\n\nfunction expandAll(): void {\n expandedPaths.value = branchPaths(nodes.value)\n}\n\nfunction collapseAll(): void {\n expandedPaths.value = []\n}\n\nconst query = ref('')\nconst treeRef = ref<{ filter: (value: string) => void } | null>(null)\n\nwatch(query, (value) => { treeRef.value?.filter(value) })\n\nfunction filter(value: string): void {\n query.value = value\n}\n\n/** Поиск идёт и по ключу, и по значению: в чужом ответе ищут то одно, то другое. */\nfunction matchKeyOrValue(value: string, data: GrJsonNode): boolean {\n if (!value) return true\n\n const needle = value.toLowerCase()\n\n return data.label.toLowerCase().includes(needle) || data.preview.toLowerCase().includes(needle)\n}\n\n/**\n * Наличие буфера уточняется после монтирования: `navigator` в теле `setup` либо\n * роняет серверный рендер, либо расходится с ним. До этого момента кнопки нет —\n * ровно то же, что отдаёт сервер, поэтому гидрация совпадает.\n */\nconst canCopy = ref(false)\n\nonMounted(() => {\n canCopy.value = typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function'\n\n if (props.virtual && props.maxHeight === undefined && typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') {\n console.warn('[GrJsonViewer] `virtual` без `maxHeight` не включает виртуализацию: окно считается от высоты области.')\n }\n})\n\nconst showCopy = computed(() => resolvedCopyable.value && canCopy.value)\n\nconst copiedPath = ref<string | null>(null)\nlet copiedTimer: number | null = null\n\nfunction resetCopiedTimer(): void {\n if (copiedTimer !== null) {\n clearTimeout(copiedTimer)\n copiedTimer = null\n }\n}\n\nonBeforeUnmount(resetCopiedTimer)\n\n/**\n * В буфер уходит **полное** значение узла, а не то, что видно: обрезка\n * принадлежит показу, и вставить её обратно нельзя.\n */\nasync function copyNode(node: GrJsonNode): Promise<void> {\n const text = typeof node.value === 'string' ? node.value : JSON.stringify(node.value, null, 2) ?? String(node.value)\n\n try {\n await navigator.clipboard.writeText(text)\n }\n catch {\n announce(t('gr.jsonViewer.copyFailed', 'Could not copy value'))\n return\n }\n\n copiedPath.value = node.path\n resetCopiedTimer()\n copiedTimer = window.setTimeout(() => {\n copiedPath.value = null\n copiedTimer = null\n }, 1200)\n\n announce(t('gr.jsonViewer.copied', 'Value copied'))\n emit('copy', { path: node.path, value: node.value })\n}\n\ndefineExpose({ filter, expandAll, collapseAll })\n</script>\n\n<template>\n <div data-gr-json-viewer :class=\"jsonViewerRootClass\">\n <div v-if=\"resolvedSearchable\" data-gr-json-viewer-toolbar :class=\"jsonViewerToolbarClass\">\n <GrInput\n v-model=\"query\"\n :class=\"jsonViewerSearchClass\"\n :size=\"resolvedSize\"\n clearable\n :placeholder=\"t('gr.jsonViewer.search', 'Search key or value')\"\n :aria-label=\"t('gr.jsonViewer.search', 'Search key or value')\"\n />\n\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"expandAll\">\n {{ t('gr.jsonViewer.expandAll', 'Expand all') }}\n </GrButton>\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"collapseAll\">\n {{ t('gr.jsonViewer.collapseAll', 'Collapse all') }}\n </GrButton>\n </div>\n\n <GrTree\n ref=\"treeRef\"\n :data=\"nodes\"\n node-key=\"path\"\n :props=\"{ children: 'children', label: 'label', isLeaf: 'isLeaf' }\"\n :default-expanded-keys=\"expandedPaths\"\n :filter-node-method=\"matchKeyOrValue\"\n :content-class=\"jsonViewerContentClass\"\n :virtual=\"virtual\"\n :max-height=\"maxHeight\"\n :size=\"resolvedSize\"\n :aria-label=\"ariaLabel\"\n >\n <template #default=\"{ data }\">\n <span data-gr-json-viewer-row :class=\"[jsonViewerRowClass, jsonViewerGroupClass]\">\n <span data-gr-json-viewer-key :class=\"jsonViewerKeyClass\">{{ data.label }}</span>\n <span :class=\"jsonViewerPunctuationClass\">:</span>\n\n <span\n data-gr-json-viewer-value\n :class=\"[jsonViewerValueClass, jsonValueClass[(data as GrJsonNode).kind]]\"\n >\n <slot name=\"leaf\" :node=\"data as GrJsonNode\">{{ (data as GrJsonNode).preview }}</slot>\n </span>\n\n <span\n v-if=\"(data as GrJsonNode).kind === 'truncation'\"\n :class=\"jsonViewerMutedClass\"\n >{{ t('gr.jsonViewer.moreItems', 'more') }}</span>\n\n <GrButton\n v-if=\"showCopy && (data as GrJsonNode).kind !== 'truncation'\"\n data-gr-json-viewer-copy\n variant=\"ghost\"\n size=\"xs\"\n square\n :class=\"jsonViewerCopyClass\"\n :aria-label=\"t('gr.jsonViewer.copy', 'Copy value')\"\n :title=\"t('gr.jsonViewer.copy', 'Copy value')\"\n @click.stop=\"copyNode(data as GrJsonNode)\"\n >\n <component\n :is=\"copiedPath === (data as GrJsonNode).path ? IconCheck : IconCopy\"\n class=\"h-3.5 w-3.5\"\n aria-hidden=\"true\"\n />\n </GrButton>\n </span>\n </template>\n </GrTree>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrList-DAtzVIHh.js","names":["$slots"],"sources":["../../../../src/components/GrList/GrList.vue","../../../../src/components/GrList/GrList.vue","../../../../src/components/GrList/GrListItem.vue","../../../../src/components/GrList/GrListItem.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"T = unknown\">\nimport { computed, onMounted, onUpdated, ref, useSlots, watch } from 'vue'\n\nimport GrCard from '../GrCard'\nimport type { GrCardVariant } from '../GrCard/grCardStyles'\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\nimport { hasMeaningfulSlotContent } from '../shared/slotNodes'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useVirtualList, type GrVirtualAlign } from '../../composables/useVirtualList'\n\nimport { dividedClass, emptyClass, listSurfaceClass, loadingRowClass } from './grListStyles'\n\nexport interface GrListProps<TItem = unknown> {\n /**\n * Поверхность списка — вариант карточки под ним. `ghost` убирает рамку и\n * тень: список внутри уже существующей карточки не должен давать вторую.\n */\n variant?: GrCardVariant\n /** Показывать ли горизонтальные разделители между элементами (по умолчанию — да). */\n divided?: boolean\n /** Идёт загрузка: вместо пунктов — строки-скелетоны, контейнер помечен `aria-busy`. */\n loading?: boolean\n /** Сколько строк-заглушек показать при `loading`. */\n loadingRows?: number\n /**\n * Список пуст. По умолчанию определяется сам — по содержимому слота; проп\n * нужен там, где потребитель знает лучше (например, слот заполнен\n * заголовками групп, а данных в них нет).\n */\n empty?: boolean\n /** Текст пустого состояния. Слот `#empty` сильнее. */\n emptyText?: string\n /**\n * Данные списка. С ними пункты рисует слот `#item`, а не слот по умолчанию, —\n * и только так список знает размер набора, то есть может его виртуализировать.\n */\n items?: TItem[]\n /** Ключ пункта для `v-for`: имя поля или функция. Без него — индекс. */\n itemKey?: string | ((item: TItem, index: number) => string | number)\n /** Высота видимой части: контейнер становится скроллером. Число — пиксели. */\n maxHeight?: number | string\n /** Держать в DOM только окно вокруг вьюпорта. Требует `items` и `maxHeight`. */\n virtual?: boolean\n /** Оценка высоты пункта до замера. Уточняется по факту при первом рендере. */\n estimatedItemSize?: number\n}\n\nconst props = withDefaults(defineProps<GrListProps<T>>(), {\n // Дефолт живёт в `GrCard` (и его резолвере `GrConfigProvider`), поэтому здесь\n // остаётся `undefined` — иначе список перебивал бы настройку карточки.\n variant: undefined,\n divided: true,\n loading: false,\n loadingRows: 3,\n empty: undefined,\n emptyText: undefined,\n items: undefined,\n itemKey: undefined,\n maxHeight: undefined,\n virtual: false,\n // Строка обычной плотности с заголовком и описанием.\n estimatedItemSize: 56,\n})\n\nconst slots = useSlots()\nconst { t } = useGranularityTranslations()\n\nconst listEl = ref<HTMLElement | null>(null)\n\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.list.empty', 'Nothing here yet'))\n\nconst dataMode = computed(() => props.items !== undefined)\n\n// Пустоту видно по слоту: `v-for` по пустому массиву оставляет фрагмент без\n// узлов, `v-if` — комментарий, и ни то ни другое пунктом списка не является.\nconst isEmpty = computed(() => {\n if (props.empty !== undefined)\n return props.empty\n\n if (dataMode.value)\n return props.items!.length === 0\n\n return !hasMeaningfulSlotContent(slots.default?.() ?? [])\n})\n\nconst showItems = computed(() => !props.loading && !isEmpty.value)\n\n// Разделители нужны только между пунктами: в пустой и загрузочной ветках они\n// оставили бы висящую линию. Клип по радиусу карточки — безусловный.\nconst listClass = computed(() => [\n listSurfaceClass,\n props.divided && showItems.value ? dividedClass : '',\n].filter(Boolean).join(' '))\n\nconst loadingRowCount = computed(() => Math.max(1, Math.trunc(props.loadingRows)))\n\nconst virtualEnabled = computed(() => props.virtual && dataMode.value && props.maxHeight !== undefined)\n\nconst virtualizer = useVirtualList({\n container: listEl,\n count: () => (virtualEnabled.value && showItems.value ? props.items!.length : 0),\n // Замена массива сбрасывает замеры; append в тот же массив их сохраняет.\n source: () => props.items,\n itemSize: () => props.estimatedItemSize,\n // Первый рендер обязан совпасть с серверным: контейнера ещё нет, и окно\n // считается от объявленной высоты, а не от измеренной.\n viewportSize: () => (typeof props.maxHeight === 'number' ? props.maxHeight : undefined),\n})\n\n/** Окно к отрисовке: при выключенной виртуализации — весь набор. */\nconst renderedItems = computed(() => {\n const items = props.items ?? []\n if (!virtualEnabled.value) return items.map((item, index) => ({ item, index }))\n\n const { start, end } = virtualizer.range.value\n return items.slice(start, end).map((item, offset) => ({ item, index: start + offset }))\n})\n\nfunction keyOf(item: T, index: number): string | number {\n const key = props.itemKey\n if (typeof key === 'function') return key(item, index)\n if (typeof key === 'string') return (item as Record<string, unknown>)[key] as string | number\n\n return index\n}\n\n/**\n * ARIA-набор для пункта.\n *\n * Только в виртуальном режиме: в DOM тогда неполный набор, и без этих атрибутов\n * диктор объявил бы «7 из 20» на списке в пять тысяч. Когда набор в DOM целиком,\n * его размер и позицию браузер считает сам, а руками проставленные атрибуты\n * начинают расходиться с разметкой при любой фильтрации.\n */\nfunction ariaOf(index: number): Record<string, number> {\n if (!virtualEnabled.value) return {}\n\n return { 'aria-setsize': props.items!.length, 'aria-posinset': index + 1 }\n}\n\nconst maxHeightStyle = computed(() => {\n const value = props.maxHeight\n if (value == null) return undefined\n return typeof value === 'number' ? `${value}px` : value\n})\n\nconst listStyle = computed(() => {\n if (props.maxHeight === undefined) return undefined\n\n const scroller = { overflowY: 'auto' as const, maxHeight: maxHeightStyle.value }\n if (!virtualEnabled.value) return scroller\n\n return { ...scroller, ...virtualizer.spacerStyle.value }\n})\n\n/**\n * Замер отрисованных пунктов.\n *\n * Идёт по детям контейнера, а не по `ref` на элементе: пункт рисует слот\n * потребителя, привязать к нему `ref` неоткуда. В виртуальном режиме прямые\n * дети контейнера — ровно окно (распорки живут псевдоэлементами и детьми не\n * считаются), поэтому сопоставление по позиции однозначно.\n */\nfunction measureRendered(): void {\n if (!virtualEnabled.value) return\n // В состояниях loading/empty дети контейнера — скелетоны и заглушки: их\n // высоты записались бы в замеры под индексами будущих строк данных.\n if (!showItems.value) return\n\n const container = listEl.value\n if (!container) return\n\n const { start } = virtualizer.range.value\n const children = container.children\n for (let offset = 0; offset < children.length; offset++) {\n virtualizer.measure(start + offset, children[offset])\n }\n}\n\nonMounted(measureRendered)\nonUpdated(measureRendered)\n\nfunction scrollToIndex(index: number, align?: GrVirtualAlign): void {\n virtualizer.scrollToIndex(index, align)\n}\n\ndefineExpose({ scrollToIndex })\n\nif (import.meta.env?.DEV) {\n watch(\n () => [props.virtual, dataMode.value, props.maxHeight] as const,\n ([virtual, hasItems, maxHeight]) => {\n if (!virtual) return\n\n if (!hasItems) {\n console.warn(\n '[granularity] GrList: `virtual` требует `items` — пункты из слота по умолчанию '\n + 'список не считает и резать окно ему не из чего.',\n )\n }\n else if (maxHeight === undefined) {\n console.warn(\n '[granularity] GrList: `virtual` требует `maxHeight` — без ограничения высоты '\n + 'вьюпорта нет, и в окно попадает весь набор.',\n )\n }\n },\n { immediate: true },\n )\n\n watch(\n () => [dataMode.value, Boolean(slots.item)] as const,\n ([hasItems, hasSlot]) => {\n if (hasItems && !hasSlot) {\n console.warn(\n '[granularity] GrList: с `items` пункты рисует слот `#item` — без него список пуст.',\n )\n }\n },\n { immediate: true },\n )\n\n onMounted(() => {\n if (!virtualEnabled.value) return\n\n const first = listEl.value?.firstElementChild\n if (first && !first.hasAttribute('aria-posinset')) {\n console.warn(\n '[granularity] GrList: при `virtual` пункты обязаны нести `aria-setsize`/`aria-posinset` — '\n + 'пробросьте `aria` из слота `#item`: <GrListItem v-bind=\"aria\" … />. Иначе диктор '\n + 'посчитает набор по отрисованному окну.',\n )\n }\n })\n}\n</script>\n\n<template>\n <GrCard :variant=\"variant\">\n <div\n ref=\"listEl\"\n data-gr-list\n role=\"list\"\n :class=\"listClass\"\n :style=\"listStyle\"\n :tabindex=\"maxHeight === undefined ? undefined : 0\"\n :data-gr-virtual=\"virtualEnabled ? '' : undefined\"\n :aria-busy=\"loading ? 'true' : undefined\"\n >\n <template v-if=\"loading\">\n <slot name=\"loading\">\n <div\n v-for=\"row in loadingRowCount\"\n :key=\"row\"\n data-gr-list-loading-row\n :class=\"loadingRowClass\"\n >\n <GrSkeleton />\n </div>\n </slot>\n </template>\n\n <div\n v-else-if=\"isEmpty\"\n data-gr-list-empty\n :class=\"emptyClass\"\n >\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <template v-else-if=\"dataMode\">\n <template\n v-for=\"entry in renderedItems\"\n :key=\"keyOf(entry.item, entry.index)\"\n >\n <slot\n name=\"item\"\n :item=\"entry.item\"\n :index=\"entry.index\"\n :aria=\"ariaOf(entry.index)\"\n />\n </template>\n </template>\n\n <slot v-else />\n </div>\n </GrCard>\n</template>\n\n<style scoped>\n/* Срезанное сверху и снизу держат псевдоэлементы контейнера: их не за что\n размонтировать, в дереве доступности их нет, а высота приезжает переменными\n в том же патче, что и пункты. Почему не отступы контейнера и не поля крайних\n пунктов — `docs/virtual-list.md`. */\n[data-gr-virtual]::before,\n[data-gr-virtual]::after {\n content: '';\n display: block;\n flex: none;\n}\n\n[data-gr-virtual]::before {\n height: var(--gr-virtual-before, 0px);\n}\n\n[data-gr-virtual]::after {\n height: var(--gr-virtual-after, 0px);\n}\n</style>\n","<script setup lang=\"ts\" generic=\"T = unknown\">\nimport { computed, onMounted, onUpdated, ref, useSlots, watch } from 'vue'\n\nimport GrCard from '../GrCard'\nimport type { GrCardVariant } from '../GrCard/grCardStyles'\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\nimport { hasMeaningfulSlotContent } from '../shared/slotNodes'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useVirtualList, type GrVirtualAlign } from '../../composables/useVirtualList'\n\nimport { dividedClass, emptyClass, listSurfaceClass, loadingRowClass } from './grListStyles'\n\nexport interface GrListProps<TItem = unknown> {\n /**\n * Поверхность списка — вариант карточки под ним. `ghost` убирает рамку и\n * тень: список внутри уже существующей карточки не должен давать вторую.\n */\n variant?: GrCardVariant\n /** Показывать ли горизонтальные разделители между элементами (по умолчанию — да). */\n divided?: boolean\n /** Идёт загрузка: вместо пунктов — строки-скелетоны, контейнер помечен `aria-busy`. */\n loading?: boolean\n /** Сколько строк-заглушек показать при `loading`. */\n loadingRows?: number\n /**\n * Список пуст. По умолчанию определяется сам — по содержимому слота; проп\n * нужен там, где потребитель знает лучше (например, слот заполнен\n * заголовками групп, а данных в них нет).\n */\n empty?: boolean\n /** Текст пустого состояния. Слот `#empty` сильнее. */\n emptyText?: string\n /**\n * Данные списка. С ними пункты рисует слот `#item`, а не слот по умолчанию, —\n * и только так список знает размер набора, то есть может его виртуализировать.\n */\n items?: TItem[]\n /** Ключ пункта для `v-for`: имя поля или функция. Без него — индекс. */\n itemKey?: string | ((item: TItem, index: number) => string | number)\n /** Высота видимой части: контейнер становится скроллером. Число — пиксели. */\n maxHeight?: number | string\n /** Держать в DOM только окно вокруг вьюпорта. Требует `items` и `maxHeight`. */\n virtual?: boolean\n /** Оценка высоты пункта до замера. Уточняется по факту при первом рендере. */\n estimatedItemSize?: number\n}\n\nconst props = withDefaults(defineProps<GrListProps<T>>(), {\n // Дефолт живёт в `GrCard` (и его резолвере `GrConfigProvider`), поэтому здесь\n // остаётся `undefined` — иначе список перебивал бы настройку карточки.\n variant: undefined,\n divided: true,\n loading: false,\n loadingRows: 3,\n empty: undefined,\n emptyText: undefined,\n items: undefined,\n itemKey: undefined,\n maxHeight: undefined,\n virtual: false,\n // Строка обычной плотности с заголовком и описанием.\n estimatedItemSize: 56,\n})\n\nconst slots = useSlots()\nconst { t } = useGranularityTranslations()\n\nconst listEl = ref<HTMLElement | null>(null)\n\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.list.empty', 'Nothing here yet'))\n\nconst dataMode = computed(() => props.items !== undefined)\n\n// Пустоту видно по слоту: `v-for` по пустому массиву оставляет фрагмент без\n// узлов, `v-if` — комментарий, и ни то ни другое пунктом списка не является.\nconst isEmpty = computed(() => {\n if (props.empty !== undefined)\n return props.empty\n\n if (dataMode.value)\n return props.items!.length === 0\n\n return !hasMeaningfulSlotContent(slots.default?.() ?? [])\n})\n\nconst showItems = computed(() => !props.loading && !isEmpty.value)\n\n// Разделители нужны только между пунктами: в пустой и загрузочной ветках они\n// оставили бы висящую линию. Клип по радиусу карточки — безусловный.\nconst listClass = computed(() => [\n listSurfaceClass,\n props.divided && showItems.value ? dividedClass : '',\n].filter(Boolean).join(' '))\n\nconst loadingRowCount = computed(() => Math.max(1, Math.trunc(props.loadingRows)))\n\nconst virtualEnabled = computed(() => props.virtual && dataMode.value && props.maxHeight !== undefined)\n\nconst virtualizer = useVirtualList({\n container: listEl,\n count: () => (virtualEnabled.value && showItems.value ? props.items!.length : 0),\n // Замена массива сбрасывает замеры; append в тот же массив их сохраняет.\n source: () => props.items,\n itemSize: () => props.estimatedItemSize,\n // Первый рендер обязан совпасть с серверным: контейнера ещё нет, и окно\n // считается от объявленной высоты, а не от измеренной.\n viewportSize: () => (typeof props.maxHeight === 'number' ? props.maxHeight : undefined),\n})\n\n/** Окно к отрисовке: при выключенной виртуализации — весь набор. */\nconst renderedItems = computed(() => {\n const items = props.items ?? []\n if (!virtualEnabled.value) return items.map((item, index) => ({ item, index }))\n\n const { start, end } = virtualizer.range.value\n return items.slice(start, end).map((item, offset) => ({ item, index: start + offset }))\n})\n\nfunction keyOf(item: T, index: number): string | number {\n const key = props.itemKey\n if (typeof key === 'function') return key(item, index)\n if (typeof key === 'string') return (item as Record<string, unknown>)[key] as string | number\n\n return index\n}\n\n/**\n * ARIA-набор для пункта.\n *\n * Только в виртуальном режиме: в DOM тогда неполный набор, и без этих атрибутов\n * диктор объявил бы «7 из 20» на списке в пять тысяч. Когда набор в DOM целиком,\n * его размер и позицию браузер считает сам, а руками проставленные атрибуты\n * начинают расходиться с разметкой при любой фильтрации.\n */\nfunction ariaOf(index: number): Record<string, number> {\n if (!virtualEnabled.value) return {}\n\n return { 'aria-setsize': props.items!.length, 'aria-posinset': index + 1 }\n}\n\nconst maxHeightStyle = computed(() => {\n const value = props.maxHeight\n if (value == null) return undefined\n return typeof value === 'number' ? `${value}px` : value\n})\n\nconst listStyle = computed(() => {\n if (props.maxHeight === undefined) return undefined\n\n const scroller = { overflowY: 'auto' as const, maxHeight: maxHeightStyle.value }\n if (!virtualEnabled.value) return scroller\n\n return { ...scroller, ...virtualizer.spacerStyle.value }\n})\n\n/**\n * Замер отрисованных пунктов.\n *\n * Идёт по детям контейнера, а не по `ref` на элементе: пункт рисует слот\n * потребителя, привязать к нему `ref` неоткуда. В виртуальном режиме прямые\n * дети контейнера — ровно окно (распорки живут псевдоэлементами и детьми не\n * считаются), поэтому сопоставление по позиции однозначно.\n */\nfunction measureRendered(): void {\n if (!virtualEnabled.value) return\n // В состояниях loading/empty дети контейнера — скелетоны и заглушки: их\n // высоты записались бы в замеры под индексами будущих строк данных.\n if (!showItems.value) return\n\n const container = listEl.value\n if (!container) return\n\n const { start } = virtualizer.range.value\n const children = container.children\n for (let offset = 0; offset < children.length; offset++) {\n virtualizer.measure(start + offset, children[offset])\n }\n}\n\nonMounted(measureRendered)\nonUpdated(measureRendered)\n\nfunction scrollToIndex(index: number, align?: GrVirtualAlign): void {\n virtualizer.scrollToIndex(index, align)\n}\n\ndefineExpose({ scrollToIndex })\n\nif (import.meta.env?.DEV) {\n watch(\n () => [props.virtual, dataMode.value, props.maxHeight] as const,\n ([virtual, hasItems, maxHeight]) => {\n if (!virtual) return\n\n if (!hasItems) {\n console.warn(\n '[granularity] GrList: `virtual` требует `items` — пункты из слота по умолчанию '\n + 'список не считает и резать окно ему не из чего.',\n )\n }\n else if (maxHeight === undefined) {\n console.warn(\n '[granularity] GrList: `virtual` требует `maxHeight` — без ограничения высоты '\n + 'вьюпорта нет, и в окно попадает весь набор.',\n )\n }\n },\n { immediate: true },\n )\n\n watch(\n () => [dataMode.value, Boolean(slots.item)] as const,\n ([hasItems, hasSlot]) => {\n if (hasItems && !hasSlot) {\n console.warn(\n '[granularity] GrList: с `items` пункты рисует слот `#item` — без него список пуст.',\n )\n }\n },\n { immediate: true },\n )\n\n onMounted(() => {\n if (!virtualEnabled.value) return\n\n const first = listEl.value?.firstElementChild\n if (first && !first.hasAttribute('aria-posinset')) {\n console.warn(\n '[granularity] GrList: при `virtual` пункты обязаны нести `aria-setsize`/`aria-posinset` — '\n + 'пробросьте `aria` из слота `#item`: <GrListItem v-bind=\"aria\" … />. Иначе диктор '\n + 'посчитает набор по отрисованному окну.',\n )\n }\n })\n}\n</script>\n\n<template>\n <GrCard :variant=\"variant\">\n <div\n ref=\"listEl\"\n data-gr-list\n role=\"list\"\n :class=\"listClass\"\n :style=\"listStyle\"\n :tabindex=\"maxHeight === undefined ? undefined : 0\"\n :data-gr-virtual=\"virtualEnabled ? '' : undefined\"\n :aria-busy=\"loading ? 'true' : undefined\"\n >\n <template v-if=\"loading\">\n <slot name=\"loading\">\n <div\n v-for=\"row in loadingRowCount\"\n :key=\"row\"\n data-gr-list-loading-row\n :class=\"loadingRowClass\"\n >\n <GrSkeleton />\n </div>\n </slot>\n </template>\n\n <div\n v-else-if=\"isEmpty\"\n data-gr-list-empty\n :class=\"emptyClass\"\n >\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <template v-else-if=\"dataMode\">\n <template\n v-for=\"entry in renderedItems\"\n :key=\"keyOf(entry.item, entry.index)\"\n >\n <slot\n name=\"item\"\n :item=\"entry.item\"\n :index=\"entry.index\"\n :aria=\"ariaOf(entry.index)\"\n />\n </template>\n </template>\n\n <slot v-else />\n </div>\n </GrCard>\n</template>\n\n<style scoped>\n/* Срезанное сверху и снизу держат псевдоэлементы контейнера: их не за что\n размонтировать, в дереве доступности их нет, а высота приезжает переменными\n в том же патче, что и пункты. Почему не отступы контейнера и не поля крайних\n пунктов — `docs/virtual-list.md`. */\n[data-gr-virtual]::before,\n[data-gr-virtual]::after {\n content: '';\n display: block;\n flex: none;\n}\n\n[data-gr-virtual]::before {\n height: var(--gr-virtual-before, 0px);\n}\n\n[data-gr-virtual]::after {\n height: var(--gr-virtual-after, 0px);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, markRaw, useSlots, watch, type Component } from 'vue'\n\nimport {\n grListItemPaddingClass,\n itemDescriptionClass,\n itemDisabledClass,\n itemHoverClass,\n itemInteractiveClass,\n itemLayoutClass,\n itemTitleClass,\n type GrListItemDensity,\n} from './grListStyles'\n\nexport type { GrListItemDensity } from './grListStyles'\n\nexport interface GrListItemProps {\n /** Заголовок строки. Если передан слот `#title`, проп игнорируется. */\n title?: string\n /** Описание под заголовком. Если передан слот `#description`, проп игнорируется. */\n description?: string\n /** Плотность вертикальных отступов: `regular` — 12px, `compact` — 8px. */\n density?: GrListItemDensity\n /** Ссылка: строка становится `<a>`. */\n href?: string\n /** Свой корневой тег строки (`RouterLink`, `Link` от Inertia). Сильнее `href`. */\n as?: string | Component\n /** Кликабельная строка без ссылки — `<button>` с событием `click`. */\n clickable?: boolean\n /** Подсветка при наведении без интерактивности. */\n hoverable?: boolean\n disabled?: boolean\n}\n\nexport interface GrListItemEmits {\n (e: 'click', event: MouseEvent): void\n}\n\nconst props = withDefaults(defineProps<GrListItemProps>(), {\n title: undefined,\n description: undefined,\n density: 'regular',\n href: undefined,\n as: undefined,\n clickable: false,\n hoverable: false,\n disabled: false,\n})\n\nconst emit = defineEmits<GrListItemEmits>()\n\nconst slots = useSlots()\n\nconst hasTitle = computed(() => !!slots.title || !!props.title)\nconst hasDescription = computed(() => !!slots.description || !!props.description)\n\nconst isInteractive = computed(() => !props.disabled && (!!props.as || !!props.href || props.clickable))\n\n/**\n * `role=\"listitem\"` остаётся на обёртке, а строка целиком — вложенный элемент:\n * `<a role=\"listitem\">` потерял бы роль ссылки, а интерактив снаружи разорвал бы\n * связку `list` → `listitem`. Обычная строка идёт по той же схеме, чтобы\n * разметка содержимого была написана один раз, а не по копии на ветку.\n */\nconst rowTag = computed<string | Component>(() => {\n if (!isInteractive.value)\n return 'div'\n\n if (props.as)\n return typeof props.as === 'string' ? props.as : markRaw(props.as)\n\n return props.href ? 'a' : 'button'\n})\n\nconst rowClass = computed(() => [\n itemLayoutClass,\n grListItemPaddingClass(props.density),\n isInteractive.value ? `${itemInteractiveClass} ${itemHoverClass}` : '',\n !isInteractive.value && props.hoverable && !props.disabled ? itemHoverClass : '',\n props.disabled ? itemDisabledClass : '',\n].filter(Boolean).join(' '))\n\nfunction onClick(event: MouseEvent): void {\n // `disabled` сюда попадает вместе с обычной строкой: и то и другое —\n // не-интерактив, а кликов не эмитит ни один.\n if (!isInteractive.value)\n return\n\n emit('click', event)\n}\n\n/** Теги, попадающие в таб-порядок сами. `<a>` — только со ссылкой. */\nfunction isFocusableTag(tag: string): boolean {\n return tag === 'button' || (tag === 'a' && !!props.href)\n}\n\nif (import.meta.env?.DEV) {\n watch(\n () => [isInteractive.value, props.as] as const,\n ([interactive, as]) => {\n // Компонент из `as` (`RouterLink`, Inertia `Link`) рендерит `<a>`, но узнать\n // это до рендера нельзя — предупреждаем только по явно названному тегу.\n if (!interactive || typeof as !== 'string' || isFocusableTag(as)) return\n\n console.warn(\n `[granularity] GrListItem: as=\"${as}\" не попадает в таб-порядок — строка кликается `\n + 'мышью, но не с клавиатуры. Возьмите тег, умеющий фокус (`button`, `a` со ссылкой), '\n + 'или компонент роутера.',\n )\n },\n { immediate: true },\n )\n}\n</script>\n\n<template>\n <div\n data-gr-list-item\n role=\"listitem\"\n :aria-disabled=\"disabled ? 'true' : undefined\"\n >\n <component\n :is=\"rowTag\"\n :data-gr-list-item-action=\"isInteractive ? '' : undefined\"\n :type=\"rowTag === 'button' ? 'button' : undefined\"\n :href=\"isInteractive ? href : undefined\"\n :class=\"rowClass\"\n @click=\"onClick\"\n >\n <div v-if=\"$slots.prefix\" class=\"shrink-0\">\n <slot name=\"prefix\" />\n </div>\n <div class=\"min-w-0 flex-1\">\n <div v-if=\"hasTitle\" :class=\"itemTitleClass\">\n <slot name=\"title\">\n{{ title }}\n</slot>\n </div>\n <div\n v-if=\"hasDescription\"\n :class=\"itemDescriptionClass\"\n >\n <slot name=\"description\">\n{{ description }}\n</slot>\n </div>\n </div>\n <div v-if=\"$slots.default\" class=\"shrink-0\">\n <slot />\n </div>\n </component>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, markRaw, useSlots, watch, type Component } from 'vue'\n\nimport {\n grListItemPaddingClass,\n itemDescriptionClass,\n itemDisabledClass,\n itemHoverClass,\n itemInteractiveClass,\n itemLayoutClass,\n itemTitleClass,\n type GrListItemDensity,\n} from './grListStyles'\n\nexport type { GrListItemDensity } from './grListStyles'\n\nexport interface GrListItemProps {\n /** Заголовок строки. Если передан слот `#title`, проп игнорируется. */\n title?: string\n /** Описание под заголовком. Если передан слот `#description`, проп игнорируется. */\n description?: string\n /** Плотность вертикальных отступов: `regular` — 12px, `compact` — 8px. */\n density?: GrListItemDensity\n /** Ссылка: строка становится `<a>`. */\n href?: string\n /** Свой корневой тег строки (`RouterLink`, `Link` от Inertia). Сильнее `href`. */\n as?: string | Component\n /** Кликабельная строка без ссылки — `<button>` с событием `click`. */\n clickable?: boolean\n /** Подсветка при наведении без интерактивности. */\n hoverable?: boolean\n disabled?: boolean\n}\n\nexport interface GrListItemEmits {\n (e: 'click', event: MouseEvent): void\n}\n\nconst props = withDefaults(defineProps<GrListItemProps>(), {\n title: undefined,\n description: undefined,\n density: 'regular',\n href: undefined,\n as: undefined,\n clickable: false,\n hoverable: false,\n disabled: false,\n})\n\nconst emit = defineEmits<GrListItemEmits>()\n\nconst slots = useSlots()\n\nconst hasTitle = computed(() => !!slots.title || !!props.title)\nconst hasDescription = computed(() => !!slots.description || !!props.description)\n\nconst isInteractive = computed(() => !props.disabled && (!!props.as || !!props.href || props.clickable))\n\n/**\n * `role=\"listitem\"` остаётся на обёртке, а строка целиком — вложенный элемент:\n * `<a role=\"listitem\">` потерял бы роль ссылки, а интерактив снаружи разорвал бы\n * связку `list` → `listitem`. Обычная строка идёт по той же схеме, чтобы\n * разметка содержимого была написана один раз, а не по копии на ветку.\n */\nconst rowTag = computed<string | Component>(() => {\n if (!isInteractive.value)\n return 'div'\n\n if (props.as)\n return typeof props.as === 'string' ? props.as : markRaw(props.as)\n\n return props.href ? 'a' : 'button'\n})\n\nconst rowClass = computed(() => [\n itemLayoutClass,\n grListItemPaddingClass(props.density),\n isInteractive.value ? `${itemInteractiveClass} ${itemHoverClass}` : '',\n !isInteractive.value && props.hoverable && !props.disabled ? itemHoverClass : '',\n props.disabled ? itemDisabledClass : '',\n].filter(Boolean).join(' '))\n\nfunction onClick(event: MouseEvent): void {\n // `disabled` сюда попадает вместе с обычной строкой: и то и другое —\n // не-интерактив, а кликов не эмитит ни один.\n if (!isInteractive.value)\n return\n\n emit('click', event)\n}\n\n/** Теги, попадающие в таб-порядок сами. `<a>` — только со ссылкой. */\nfunction isFocusableTag(tag: string): boolean {\n return tag === 'button' || (tag === 'a' && !!props.href)\n}\n\nif (import.meta.env?.DEV) {\n watch(\n () => [isInteractive.value, props.as] as const,\n ([interactive, as]) => {\n // Компонент из `as` (`RouterLink`, Inertia `Link`) рендерит `<a>`, но узнать\n // это до рендера нельзя — предупреждаем только по явно названному тегу.\n if (!interactive || typeof as !== 'string' || isFocusableTag(as)) return\n\n console.warn(\n `[granularity] GrListItem: as=\"${as}\" не попадает в таб-порядок — строка кликается `\n + 'мышью, но не с клавиатуры. Возьмите тег, умеющий фокус (`button`, `a` со ссылкой), '\n + 'или компонент роутера.',\n )\n },\n { immediate: true },\n )\n}\n</script>\n\n<template>\n <div\n data-gr-list-item\n role=\"listitem\"\n :aria-disabled=\"disabled ? 'true' : undefined\"\n >\n <component\n :is=\"rowTag\"\n :data-gr-list-item-action=\"isInteractive ? '' : undefined\"\n :type=\"rowTag === 'button' ? 'button' : undefined\"\n :href=\"isInteractive ? href : undefined\"\n :class=\"rowClass\"\n @click=\"onClick\"\n >\n <div v-if=\"$slots.prefix\" class=\"shrink-0\">\n <slot name=\"prefix\" />\n </div>\n <div class=\"min-w-0 flex-1\">\n <div v-if=\"hasTitle\" :class=\"itemTitleClass\">\n <slot name=\"title\">\n{{ title }}\n</slot>\n </div>\n <div\n v-if=\"hasDescription\"\n :class=\"itemDescriptionClass\"\n >\n <slot name=\"description\">\n{{ description }}\n</slot>\n </div>\n </div>\n <div v-if=\"$slots.default\" class=\"shrink-0\">\n <slot />\n </div>\n </component>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+CA,MAAM,QAAQ;EAiBd,MAAM,QAAQ,SAAS;EACvB,MAAM,EAAE,MAAM,2BAA2B;EAEzC,MAAM,SAAS,IAAwB,IAAI;EAE3C,MAAM,oBAAoB,eAAe,MAAM,aAAa,EAAE,iBAAiB,kBAAkB,CAAC;EAElG,MAAM,WAAW,eAAe,MAAM,UAAU,KAAA,CAAS;EAIzD,MAAM,UAAU,eAAe;GAC7B,IAAI,MAAM,UAAU,KAAA,GAClB,OAAO,MAAM;GAEf,IAAI,SAAS,OACX,OAAO,MAAM,MAAO,WAAW;GAEjC,OAAO,CAAC,yBAAyB,MAAM,UAAU,KAAK,CAAC,CAAC;EAC1D,CAAC;EAED,MAAM,YAAY,eAAe,CAAC,MAAM,WAAW,CAAC,QAAQ,KAAK;EAIjE,MAAM,YAAY,eAAe,CAC/B,kBACA,MAAM,WAAW,UAAU,QAAQ,eAAe,EACpD,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,CAAC;EAE3B,MAAM,kBAAkB,eAAe,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,WAAW,CAAC,CAAC;EAEjF,MAAM,iBAAiB,eAAe,MAAM,WAAW,SAAS,SAAS,MAAM,cAAc,KAAA,CAAS;EAEtG,MAAM,cAAc,eAAe;GACjC,WAAW;GACX,aAAc,eAAe,SAAS,UAAU,QAAQ,MAAM,MAAO,SAAS;GAE9E,cAAc,MAAM;GACpB,gBAAgB,MAAM;GAGtB,oBAAqB,OAAO,MAAM,cAAc,WAAW,MAAM,YAAY,KAAA;EAC/E,CAAC;;EAGD,MAAM,gBAAgB,eAAe;GACnC,MAAM,QAAQ,MAAM,SAAS,CAAC;GAC9B,IAAI,CAAC,eAAe,OAAO,OAAO,MAAM,KAAK,MAAM,WAAW;IAAE;IAAM;GAAM,EAAE;GAE9E,MAAM,EAAE,OAAO,QAAQ,YAAY,MAAM;GACzC,OAAO,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,MAAM,YAAY;IAAE;IAAM,OAAO,QAAQ;GAAO,EAAE;EACxF,CAAC;EAED,SAAS,MAAM,MAAS,OAAgC;GACtD,MAAM,MAAM,MAAM;GAClB,IAAI,OAAO,QAAQ,YAAY,OAAO,IAAI,MAAM,KAAK;GACrD,IAAI,OAAO,QAAQ,UAAU,OAAQ,KAAiC;GAEtE,OAAO;EACT;;;;;;;;;EAUA,SAAS,OAAO,OAAuC;GACrD,IAAI,CAAC,eAAe,OAAO,OAAO,CAAC;GAEnC,OAAO;IAAE,gBAAgB,MAAM,MAAO;IAAQ,iBAAiB,QAAQ;GAAE;EAC3E;EAEA,MAAM,iBAAiB,eAAe;GACpC,MAAM,QAAQ,MAAM;GACpB,IAAI,SAAS,MAAM,OAAO,KAAA;GAC1B,OAAO,OAAO,UAAU,WAAW,GAAG,MAAM,MAAM;EACpD,CAAC;EAED,MAAM,YAAY,eAAe;GAC/B,IAAI,MAAM,cAAc,KAAA,GAAW,OAAO,KAAA;GAE1C,MAAM,WAAW;IAAE,WAAW;IAAiB,WAAW,eAAe;GAAM;GAC/E,IAAI,CAAC,eAAe,OAAO,OAAO;GAElC,OAAO;IAAE,GAAG;IAAU,GAAG,YAAY,YAAY;GAAM;EACzD,CAAC;;;;;;;;;EAUD,SAAS,kBAAwB;GAC/B,IAAI,CAAC,eAAe,OAAO;GAG3B,IAAI,CAAC,UAAU,OAAO;GAEtB,MAAM,YAAY,OAAO;GACzB,IAAI,CAAC,WAAW;GAEhB,MAAM,EAAE,UAAU,YAAY,MAAM;GACpC,MAAM,WAAW,UAAU;GAC3B,KAAK,IAAI,SAAS,GAAG,SAAS,SAAS,QAAQ,UAC7C,YAAY,QAAQ,QAAQ,QAAQ,SAAS,OAAO;EAExD;EAEA,UAAU,eAAe;EACzB,UAAU,eAAe;EAEzB,SAAS,cAAc,OAAe,OAA8B;GAClE,YAAY,cAAc,OAAO,KAAK;EACxC;EAEA,SAAa,EAAE,cAAc,CAAC;;uBAoD5B,YAkDS,MAAA,cAAA,GAAA,EAlDA,SAAS,QAAA,QAAO,GAAA;2BAiDjB,CAhDN,mBAgDM,OAAA;cA/CA;KAAJ,KAAI;KACJ,gBAAA;KACA,MAAK;KACJ,OAAK,eAAE,UAAA,KAAS;KAChB,OAAK,eAAE,UAAA,KAAS;KAChB,UAAU,QAAA,cAAc,KAAA,IAAY,KAAA,IAAS;KAC7C,mBAAiB,eAAA,QAAc,KAAQ,KAAA;KACvC,aAAW,QAAA,UAAO,SAAY,KAAA;QAEf,QAAA,UACd,WASO,KAAA,QAAA,WAAA,CAAA,SAAA,EAAA,UAAA,IAAA,GARL,mBAOM,UAAA,MAAA,WANU,gBAAA,QAAP,QAAG;yBADZ,mBAOM,OAAA;MALH,KAAK;MACN,4BAAA;MACC,OAAK,eAAE,MAAA,eAAA,CAAe;SAEvB,YAAc,kBAAA,CAAA,GAAA,CAAA;2BAMP,QAAA,SAAA,UAAA,GADb,mBAQM,OAAA;;KANJ,sBAAA;KACC,OAAK,eAAE,MAAA,UAAA,CAAU;QAElB,WAEO,KAAA,QAAA,SAAA,CAAA,SAAA,CAAA,gBAAA,gBADF,kBAAA,KAAiB,GAAA,CAAA,CAAA,GAAA,IAAA,CAAA,GAAA,CAAA,KAIH,SAAA,SAAA,UAAA,IAAA,GACnB,mBAUW,UAAA,EAAA,KAAA,EAAA,GAAA,WATO,cAAA,QAAT,UAAK;YAGZ,WAKE,KAAA,QAAA,QAAA;MAHC,MAAM,MAAM;MACZ,OAAO,MAAM;MACb,MAAM,OAAO,MAAM,KAAK;sBANrB,MAAM,MAAM,MAAM,MAAM,KAAK,CAAA;gBAWvC,WAAe,KAAA,QAAA,WAAA,CAAA,GAAA,KAAA,GAAA,MAAA,CAAA,CAAA,GAAA,IAAA,YAAA,CAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EExPrB,MAAM,QAAQ;EAWd,MAAM,OAAO;EAEb,MAAM,QAAQ,SAAS;EAEvB,MAAM,WAAW,eAAe,CAAC,CAAC,MAAM,SAAS,CAAC,CAAC,MAAM,KAAK;EAC9D,MAAM,iBAAiB,eAAe,CAAC,CAAC,MAAM,eAAe,CAAC,CAAC,MAAM,WAAW;EAEhF,MAAM,gBAAgB,eAAe,CAAC,MAAM,aAAa,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC,MAAM,QAAQ,MAAM,UAAU;;;;;;;EAQvG,MAAM,SAAS,eAAmC;GAChD,IAAI,CAAC,cAAc,OACjB,OAAO;GAET,IAAI,MAAM,IACR,OAAO,OAAO,MAAM,OAAO,WAAW,MAAM,KAAK,QAAQ,MAAM,EAAE;GAEnE,OAAO,MAAM,OAAO,MAAM;EAC5B,CAAC;EAED,MAAM,WAAW,eAAe;GAC9B;GACA,uBAAuB,MAAM,OAAO;GACpC,cAAc,QAAQ,GAAG,qBAAqB,GAAG,mBAAmB;GACpE,CAAC,cAAc,SAAS,MAAM,aAAa,CAAC,MAAM,WAAW,iBAAiB;GAC9E,MAAM,WAAW,oBAAoB;EACvC,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,CAAC;EAE3B,SAAS,QAAQ,OAAyB;GAGxC,IAAI,CAAC,cAAc,OACjB;GAEF,KAAK,SAAS,KAAK;EACrB;;uBA2BE,mBAmCM,OAAA;IAlCJ,qBAAA;IACA,MAAK;IACJ,iBAAe,QAAA,WAAQ,SAAY,KAAA;qBAEpC,YA6BY,wBA5BL,OAAA,KAAM,GAAA;IACV,4BAA0B,cAAA,QAAa,KAAQ,KAAA;IAC/C,MAAM,OAAA,UAAM,WAAA,WAA2B,KAAA;IACvC,MAAM,cAAA,QAAgB,QAAA,OAAO,KAAA;IAC7B,OAAK,eAAE,SAAA,KAAQ;IACR;;2BAIF;KAFKA,KAAAA,OAAO,UAAA,UAAA,GAAlB,mBAEM,OAFN,YAEM,CADJ,WAAsB,KAAA,QAAA,QAAA,CAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;KAExB,mBAcM,OAdN,YAcM,CAbO,SAAA,SAAA,UAAA,GAAX,mBAIM,OAAA;;MAJgB,OAAK,eAAE,MAAA,cAAA,CAAc;SACzC,WAEH,KAAA,QAAA,SAAA,CAAA,SAAA,CAAA,gBAAA,gBADJ,QAAA,KAAK,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,GAIQ,eAAA,SAAA,UAAA,GADR,mBAOM,OAAA;;MALH,OAAK,eAAE,MAAA,oBAAA,CAAoB;SAE5B,WAEH,KAAA,QAAA,eAAA,CAAA,SAAA,CAAA,gBAAA,gBADJ,QAAA,WAAW,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,CAAA;KAIGA,KAAAA,OAAO,WAAA,UAAA,GAAlB,mBAEM,OAFN,YAEM,CADJ,WAAQ,KAAA,QAAA,SAAA,CAAA,CAAA,KAAA,mBAAA,IAAA,IAAA"}
|