@feugene/granularity 0.20.0 → 0.22.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-BJ7Bv_G2.js → config-BC8d_smz.js} +10 -2
- package/dist/chunks/config-BC8d_smz.js.map +1 -0
- package/dist/chunks/config-BDfTXHo1.js.map +1 -0
- package/dist/chunks/config-BEVBsvni2.js.map +1 -0
- package/dist/chunks/config-BTCOJO5z2.js.map +1 -0
- package/dist/chunks/config-B_qi4Fma.js +92 -0
- package/dist/chunks/config-B_qi4Fma.js.map +1 -0
- package/dist/chunks/{config-gwro_7yn.js → config-Bm_Sovla.js} +6 -2
- package/dist/chunks/config-Bm_Sovla.js.map +1 -0
- package/dist/chunks/{config-BKvX0q10.js → config-C5mO5DYZ.js} +10 -1
- package/dist/chunks/config-C5mO5DYZ.js.map +1 -0
- package/dist/chunks/config-CIcynHGr.js +96 -0
- package/dist/chunks/config-CIcynHGr.js.map +1 -0
- package/dist/chunks/{config-BsuDM3Hb.js → config-CKULiuvc.js} +15 -1
- package/dist/chunks/config-CKULiuvc.js.map +1 -0
- package/dist/chunks/config-COpuWGr_.js.map +1 -0
- package/dist/chunks/config-DBQQT-WP.js +66 -0
- package/dist/chunks/config-DBQQT-WP.js.map +1 -0
- package/dist/chunks/config-DU2DNSRx.js.map +1 -0
- package/dist/chunks/config-QJ54_fcM.js +61 -0
- package/dist/chunks/config-QJ54_fcM.js.map +1 -0
- package/dist/chunks/config-ScuvHw6k.js.map +1 -0
- package/dist/chunks/config-eXB87DOi.js.map +1 -0
- package/dist/chunks/config-jyZq8XKL.js +58 -0
- package/dist/chunks/config-jyZq8XKL.js.map +1 -0
- package/dist/chunks/copy-DChChcq-.js +29 -0
- package/dist/chunks/copy-DChChcq-.js.map +1 -0
- package/dist/chunks/{en-COdzxgAX.js → en-DCCFP4qu.js} +18 -2
- package/dist/chunks/{en-COdzxgAX.js.map → en-DCCFP4qu.js.map} +1 -1
- package/dist/chunks/{es-ClqJ-4hr.js → es-CT34HBVM.js} +18 -2
- package/dist/chunks/{es-ClqJ-4hr.js.map → es-CT34HBVM.js.map} +1 -1
- package/dist/chunks/{es-DGMHwknJ.js → es-DdJsxlUr.js} +3 -3
- package/dist/chunks/{es-DGMHwknJ.js.map → es-DdJsxlUr.js.map} +1 -1
- package/dist/chunks/{generated-BRkbhyxk.js → generated-CiCrGDQO.js} +147 -0
- package/dist/chunks/{generated-BRkbhyxk.js.map → generated-CiCrGDQO.js.map} +1 -1
- package/dist/chunks/{granular-provider-CZYm70Pm.js → granular-provider-CYroWMjO.js} +21 -11
- package/dist/chunks/granular-provider-CYroWMjO.js.map +1 -0
- package/dist/chunks/{ru-9z8ix45c.js → ru-CLiAMwrx.js} +18 -2
- package/dist/chunks/{ru-9z8ix45c.js.map → ru-CLiAMwrx.js.map} +1 -1
- package/dist/chunks/trending-up-Bf9AQmoq.js +42 -0
- package/dist/chunks/trending-up-Bf9AQmoq.js.map +1 -0
- package/dist/components/GrCard/chunks/GrCard-Bc7nRfbe.js +144 -0
- package/dist/components/GrCard/chunks/GrCard-Bc7nRfbe.js.map +1 -0
- package/dist/components/GrCard/index.js +2 -2
- package/dist/components/GrCodeBlock/chunks/GrCodeBlock-BmXCLgLS.js +315 -0
- package/dist/components/GrCodeBlock/chunks/GrCodeBlock-BmXCLgLS.js.map +1 -0
- package/dist/components/GrCodeBlock/index.js +3 -0
- package/dist/components/GrCodeBlock/styles.css +1 -0
- package/dist/components/GrCollapse/chunks/{GrCollapse-KFcrji1L.js → GrCollapse-C1rCfbc0.js} +2 -2
- package/dist/components/GrCollapse/chunks/{GrCollapse-KFcrji1L.js.map → GrCollapse-C1rCfbc0.js.map} +1 -1
- package/dist/components/GrCollapse/index.js +2 -2
- package/dist/components/GrDataTable/chunks/{GrDataTable-CNvZg2HZ.js → GrDataTable-B5MmZFUg.js} +67 -5
- package/dist/components/GrDataTable/chunks/GrDataTable-B5MmZFUg.js.map +1 -0
- package/dist/components/GrDataTable/index.js +2 -2
- package/dist/components/GrDelta/chunks/GrDelta-BMmpYz8K.js +0 -0
- package/dist/components/GrDelta/chunks/GrDelta-BMmpYz8K.js.map +1 -0
- package/dist/components/GrDelta/index.js +3 -0
- package/dist/components/GrDescriptionList/chunks/GrDescriptionList-pXh1LZEe.js +103 -0
- package/dist/components/GrDescriptionList/chunks/GrDescriptionList-pXh1LZEe.js.map +1 -0
- package/dist/components/GrDescriptionList/index.js +3 -0
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-LOaFSCyd.js → GrDropdownMenu-DyS6dWDp.js} +2 -2
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-LOaFSCyd.js.map → GrDropdownMenu-DyS6dWDp.js.map} +1 -1
- package/dist/components/GrDropdownMenu/index.js +2 -2
- package/dist/components/GrFilePreview/chunks/GrFilePreview-rbE3zL9w.js +315 -0
- package/dist/components/GrFilePreview/chunks/GrFilePreview-rbE3zL9w.js.map +1 -0
- package/dist/components/GrFilePreview/index.js +3 -0
- package/dist/components/GrForm/chunks/{GrForm-DdJw23Pz.js → GrForm-B_Qq_13k.js} +14 -7
- package/dist/components/GrForm/chunks/GrForm-B_Qq_13k.js.map +1 -0
- package/dist/components/GrForm/index.js +1 -1
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-CMO10TS7.js.map → GrImageViewer-ng7el4wn.js.map} +1 -1
- package/dist/components/GrImageViewer/index.js +1 -1
- package/dist/components/GrInputTag/chunks/{GrInputTag-Dghhg03-.js.map → GrInputTag-C7QVPijj.js.map} +1 -1
- package/dist/components/GrInputTag/index.js +1 -1
- package/dist/components/GrJsonViewer/chunks/GrJsonViewer-DFUppRNb.js +389 -0
- package/dist/components/GrJsonViewer/chunks/GrJsonViewer-DFUppRNb.js.map +1 -0
- package/dist/components/GrJsonViewer/index.js +3 -0
- package/dist/components/GrList/chunks/{GrList-CHibO0xA.js → GrList-BRYhpqIe.js} +4 -4
- package/dist/components/GrList/chunks/GrList-BRYhpqIe.js.map +1 -0
- package/dist/components/GrList/index.js +2 -2
- package/dist/components/GrList/styles.css +1 -1
- package/dist/components/GrRadio/index.js +1 -1
- package/dist/components/GrSegmented/chunks/{GrSegmented-DP9WMAbm.js → GrSegmented-D1mjxF_O.js} +1 -1
- package/dist/components/GrSegmented/chunks/GrSegmented-D1mjxF_O.js.map +1 -0
- package/dist/components/GrSegmented/index.js +2 -2
- package/dist/components/GrSidebar/chunks/{GrSidebar-DG_t4aPE.js → GrSidebar-D7WxRtIu.js} +7 -1
- package/dist/components/GrSidebar/chunks/{GrSidebar-DG_t4aPE.js.map → GrSidebar-D7WxRtIu.js.map} +1 -1
- package/dist/components/GrSidebar/index.js +1 -1
- package/dist/components/GrSortableList/chunks/{GrSortableList-DJFPdUVv.js → GrSortableList-DPjxilmN.js} +1 -1
- package/dist/components/GrSortableList/chunks/{GrSortableList-DJFPdUVv.js.map → GrSortableList-DPjxilmN.js.map} +1 -1
- package/dist/components/GrSortableList/index.js +1 -1
- package/dist/components/GrStatistic/chunks/{GrStatistic-C3VpykHo.js → GrStatistic-DkzWUAw1.js} +8 -41
- package/dist/components/GrStatistic/chunks/GrStatistic-DkzWUAw1.js.map +1 -0
- package/dist/components/GrStatistic/index.js +1 -1
- package/dist/components/GrTabPanels/index.js +1 -1
- package/dist/components/GrTextarea/index.js +1 -1
- package/dist/components/GrTimeline/chunks/{GrTimeline-DlTraRn5.js → GrTimeline-Ch9sHzwL.js} +1 -1
- package/dist/components/GrTimeline/chunks/GrTimeline-Ch9sHzwL.js.map +1 -0
- package/dist/components/GrTimeline/index.js +2 -2
- package/dist/components/GrTimeline/styles.css +1 -1
- package/dist/components/GrToaster/chunks/{GrToaster-Db2NgrXu.js → GrToaster-DJUzx3sR.js} +1 -1
- package/dist/components/GrToaster/chunks/{GrToaster-Db2NgrXu.js.map → GrToaster-DJUzx3sR.js.map} +1 -1
- package/dist/components/GrToaster/index.js +2 -2
- package/dist/composables/useToast.js +14 -2
- 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 +38 -28
- package/dist/theme.js +1 -1
- package/dist/tokens.js +1 -1
- package/dist/types/components/GrButton/GrButton.vue.d.ts +1 -1
- package/dist/types/components/GrCard/GrCard.vue.d.ts +25 -6
- package/dist/types/components/GrCard/defaults.d.ts +2 -1
- package/dist/types/components/GrCard/grCardStyles.d.ts +8 -0
- package/dist/types/components/GrCard/index.d.ts +1 -1
- package/dist/types/components/GrCodeBlock/GrCodeBlock.vue.d.ts +79 -0
- package/dist/types/components/GrCodeBlock/config.d.ts +1 -0
- package/dist/types/components/GrCodeBlock/defaults.d.ts +21 -0
- package/dist/types/components/GrCodeBlock/grCodeBlockStyles.d.ts +53 -0
- package/dist/types/components/GrCodeBlock/index.d.ts +6 -0
- package/dist/types/components/GrCodeBlock/safelist.d.ts +1 -0
- package/dist/types/components/GrCodeBlock/serializeCode.d.ts +19 -0
- package/dist/types/components/GrCodeBlock/tokenizeJson.d.ts +24 -0
- package/dist/types/components/GrCollapse/GrCollapse.vue.d.ts +2 -2
- package/dist/types/components/GrCommandPalette/GrCommandPalette.vue.d.ts +2 -2
- package/dist/types/components/GrDataTable/GrDataTable.vue.d.ts +38 -1
- package/dist/types/components/GrDataTable/grDataTableStyles.d.ts +7 -0
- package/dist/types/components/GrDataTable/index.d.ts +1 -1
- package/dist/types/components/GrDelta/GrDelta.vue.d.ts +84 -0
- package/dist/types/components/GrDelta/config.d.ts +1 -0
- package/dist/types/components/GrDelta/defaults.d.ts +22 -0
- package/dist/types/components/GrDelta/deltaTone.d.ts +24 -0
- package/dist/types/components/GrDelta/grDeltaStyles.d.ts +38 -0
- package/dist/types/components/GrDelta/index.d.ts +6 -0
- package/dist/types/components/GrDelta/safelist.d.ts +1 -0
- package/dist/types/components/GrDescriptionList/GrDescriptionList.vue.d.ts +107 -0
- package/dist/types/components/GrDescriptionList/config.d.ts +1 -0
- package/dist/types/components/GrDescriptionList/defaults.d.ts +21 -0
- package/dist/types/components/GrDescriptionList/grDescriptionListStyles.d.ts +25 -0
- package/dist/types/components/GrDescriptionList/index.d.ts +6 -0
- package/dist/types/components/GrDescriptionList/safelist.d.ts +1 -0
- package/dist/types/components/GrDropdownMenu/GrDropdownMenu.vue.d.ts +1 -1
- package/dist/types/components/GrDropdownMenu/grDropdownMenuStyles.d.ts +10 -1
- package/dist/types/components/GrFilePreview/GrFilePreview.vue.d.ts +100 -0
- package/dist/types/components/GrFilePreview/config.d.ts +1 -0
- package/dist/types/components/GrFilePreview/defaults.d.ts +20 -0
- package/dist/types/components/GrFilePreview/fileKindOf.d.ts +14 -0
- package/dist/types/components/GrFilePreview/grFilePreviewStyles.d.ts +37 -0
- package/dist/types/components/GrFilePreview/index.d.ts +6 -0
- package/dist/types/components/GrFilePreview/safelist.d.ts +1 -0
- package/dist/types/components/GrForm/GrForm.vue.d.ts +47 -86
- package/dist/types/components/GrFormField/GrFormField.vue.d.ts +2 -2
- package/dist/types/components/GrImageViewer/GrImageViewer.vue.d.ts +1 -1
- package/dist/types/components/GrInput/GrInput.vue.d.ts +1 -1
- package/dist/types/components/GrInputTag/GrInputTag.vue.d.ts +1 -1
- package/dist/types/components/GrJsonViewer/GrJsonViewer.vue.d.ts +129 -0
- package/dist/types/components/GrJsonViewer/config.d.ts +1 -0
- package/dist/types/components/GrJsonViewer/defaults.d.ts +23 -0
- package/dist/types/components/GrJsonViewer/grJsonViewerStyles.d.ts +57 -0
- package/dist/types/components/GrJsonViewer/index.d.ts +9 -0
- package/dist/types/components/GrJsonViewer/jsonToNodes.d.ts +47 -0
- package/dist/types/components/GrJsonViewer/safelist.d.ts +1 -0
- package/dist/types/components/GrList/grListStyles.d.ts +13 -0
- package/dist/types/components/GrNumberInput/GrNumberInput.vue.d.ts +1 -1
- package/dist/types/components/GrRating/GrRating.vue.d.ts +1 -1
- package/dist/types/components/GrSegmented/GrSegmented.vue.d.ts +1 -1
- package/dist/types/components/GrStatistic/GrStatistic.vue.d.ts +2 -2
- package/dist/types/components/GrSwitch/GrSwitch.vue.d.ts +1 -1
- package/dist/types/components/GrTable/GrTable.vue.d.ts +2 -2
- package/dist/types/components/GrTabs/GrTabs.vue.d.ts +1 -1
- package/dist/types/components/GrTimeline/GrTimelineItem.vue.d.ts +3 -3
- package/dist/types/components/shared/sizes.d.ts +5 -1
- package/dist/types/composables/useToast.d.ts +11 -0
- package/dist/types/granular-provider/shared.d.ts +5 -0
- package/dist/types/i18n/messages/en.d.ts +28 -0
- package/dist/types/i18n/messages/es.d.ts +28 -0
- package/dist/types/i18n/messages/ru.d.ts +28 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/web-types.json +661 -124
- package/package.json +22 -1
- package/dist/chunks/config-BDfTXHo12.js.map +0 -1
- package/dist/chunks/config-BEVBsvni.js.map +0 -1
- package/dist/chunks/config-BJ7Bv_G2.js.map +0 -1
- package/dist/chunks/config-BKvX0q10.js.map +0 -1
- package/dist/chunks/config-BTCOJO5z.js.map +0 -1
- package/dist/chunks/config-BsuDM3Hb.js.map +0 -1
- package/dist/chunks/config-COpuWGr_2.js.map +0 -1
- package/dist/chunks/config-DU2DNSRx2.js.map +0 -1
- package/dist/chunks/config-ScuvHw6k2.js.map +0 -1
- package/dist/chunks/config-eXB87DOi2.js.map +0 -1
- package/dist/chunks/config-gwro_7yn.js.map +0 -1
- package/dist/chunks/granular-provider-CZYm70Pm.js.map +0 -1
- package/dist/components/GrCard/chunks/GrCard-DfIZOP5N.js +0 -89
- package/dist/components/GrCard/chunks/GrCard-DfIZOP5N.js.map +0 -1
- package/dist/components/GrDataTable/chunks/GrDataTable-CNvZg2HZ.js.map +0 -1
- package/dist/components/GrForm/chunks/GrForm-DdJw23Pz.js.map +0 -1
- package/dist/components/GrList/chunks/GrList-CHibO0xA.js.map +0 -1
- package/dist/components/GrSegmented/chunks/GrSegmented-DP9WMAbm.js.map +0 -1
- package/dist/components/GrStatistic/chunks/GrStatistic-C3VpykHo.js.map +0 -1
- package/dist/components/GrTimeline/chunks/GrTimeline-DlTraRn5.js.map +0 -1
- package/dist/chunks/{config-BDfTXHo12.js → config-BDfTXHo1.js} +0 -0
- package/dist/chunks/{config-BEVBsvni.js → config-BEVBsvni2.js} +0 -0
- package/dist/chunks/{config-BTCOJO5z.js → config-BTCOJO5z2.js} +0 -0
- package/dist/chunks/{config-COpuWGr_2.js → config-COpuWGr_.js} +0 -0
- package/dist/chunks/{config-DU2DNSRx2.js → config-DU2DNSRx.js} +0 -0
- package/dist/chunks/{config-ScuvHw6k2.js → config-ScuvHw6k.js} +0 -0
- package/dist/chunks/{config-eXB87DOi2.js → config-eXB87DOi.js} +0 -0
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-CMO10TS7.js → GrImageViewer-ng7el4wn.js} +1 -1
- package/dist/components/GrInputTag/chunks/{GrInputTag-Dghhg03-.js → GrInputTag-C7QVPijj.js} +1 -1
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { i as useGrComponentProp } from "../../../chunks/context-CTbuwcYs.js";
|
|
2
|
+
import { t as GrSkeleton_default } from "../../GrSkeleton/chunks/GrSkeleton-CgfXxPQ2.js";
|
|
3
|
+
import { a as filePreviewInteractiveClass, c as filePreviewMediaLoadingClass, d as filePreviewTileWidths, i as filePreviewIconClass, l as filePreviewRatioClass, o as filePreviewLabelClass, r as filePreviewFallbackClass, s as filePreviewMediaClass, u as filePreviewRootClass } from "../../../chunks/config-QJ54_fcM.js";
|
|
4
|
+
import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, defineComponent, markRaw, normalizeClass, normalizeStyle, openBlock, ref, resolveDynamicComponent, toDisplayString, unref, watch, withCtx } from "vue";
|
|
5
|
+
//#region ~icons/lucide/file
|
|
6
|
+
var _hoisted_1$6 = {
|
|
7
|
+
viewBox: "0 0 24 24",
|
|
8
|
+
width: "1.2em",
|
|
9
|
+
height: "1.2em"
|
|
10
|
+
};
|
|
11
|
+
function render$6(_ctx, _cache) {
|
|
12
|
+
return openBlock(), createElementBlock("svg", _hoisted_1$6, [..._cache[0] || (_cache[0] = [createElementVNode("g", {
|
|
13
|
+
fill: "none",
|
|
14
|
+
stroke: "currentColor",
|
|
15
|
+
"stroke-linecap": "round",
|
|
16
|
+
"stroke-linejoin": "round",
|
|
17
|
+
"stroke-width": "2"
|
|
18
|
+
}, [createElementVNode("path", { d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" }), createElementVNode("path", { d: "M14 2v5a1 1 0 0 0 1 1h5" })], -1)])]);
|
|
19
|
+
}
|
|
20
|
+
var file_default = markRaw({
|
|
21
|
+
name: "lucide-file",
|
|
22
|
+
render: render$6
|
|
23
|
+
});
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region ~icons/lucide/file-archive
|
|
26
|
+
var _hoisted_1$5 = {
|
|
27
|
+
viewBox: "0 0 24 24",
|
|
28
|
+
width: "1.2em",
|
|
29
|
+
height: "1.2em"
|
|
30
|
+
};
|
|
31
|
+
function render$5(_ctx, _cache) {
|
|
32
|
+
return openBlock(), createElementBlock("svg", _hoisted_1$5, [..._cache[0] || (_cache[0] = [createElementVNode("g", {
|
|
33
|
+
fill: "none",
|
|
34
|
+
stroke: "currentColor",
|
|
35
|
+
"stroke-linecap": "round",
|
|
36
|
+
"stroke-linejoin": "round",
|
|
37
|
+
"stroke-width": "2"
|
|
38
|
+
}, [
|
|
39
|
+
createElementVNode("path", { d: "M13.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v11.5" }),
|
|
40
|
+
createElementVNode("path", { d: "M14 2v5a1 1 0 0 0 1 1h5M8 12v-1m0 7v-2m0-9V6" }),
|
|
41
|
+
createElementVNode("circle", {
|
|
42
|
+
cx: "8",
|
|
43
|
+
cy: "20",
|
|
44
|
+
r: "2"
|
|
45
|
+
})
|
|
46
|
+
], -1)])]);
|
|
47
|
+
}
|
|
48
|
+
var file_archive_default = markRaw({
|
|
49
|
+
name: "lucide-file-archive",
|
|
50
|
+
render: render$5
|
|
51
|
+
});
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region ~icons/lucide/file-spreadsheet
|
|
54
|
+
var _hoisted_1$4 = {
|
|
55
|
+
viewBox: "0 0 24 24",
|
|
56
|
+
width: "1.2em",
|
|
57
|
+
height: "1.2em"
|
|
58
|
+
};
|
|
59
|
+
function render$4(_ctx, _cache) {
|
|
60
|
+
return openBlock(), createElementBlock("svg", _hoisted_1$4, [..._cache[0] || (_cache[0] = [createElementVNode("g", {
|
|
61
|
+
fill: "none",
|
|
62
|
+
stroke: "currentColor",
|
|
63
|
+
"stroke-linecap": "round",
|
|
64
|
+
"stroke-linejoin": "round",
|
|
65
|
+
"stroke-width": "2"
|
|
66
|
+
}, [createElementVNode("path", { d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" }), createElementVNode("path", { d: "M14 2v5a1 1 0 0 0 1 1h5M8 13h2m4 0h2m-8 4h2m4 0h2" })], -1)])]);
|
|
67
|
+
}
|
|
68
|
+
var file_spreadsheet_default = markRaw({
|
|
69
|
+
name: "lucide-file-spreadsheet",
|
|
70
|
+
render: render$4
|
|
71
|
+
});
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region ~icons/lucide/file-text
|
|
74
|
+
var _hoisted_1$3 = {
|
|
75
|
+
viewBox: "0 0 24 24",
|
|
76
|
+
width: "1.2em",
|
|
77
|
+
height: "1.2em"
|
|
78
|
+
};
|
|
79
|
+
function render$3(_ctx, _cache) {
|
|
80
|
+
return openBlock(), createElementBlock("svg", _hoisted_1$3, [..._cache[0] || (_cache[0] = [createElementVNode("g", {
|
|
81
|
+
fill: "none",
|
|
82
|
+
stroke: "currentColor",
|
|
83
|
+
"stroke-linecap": "round",
|
|
84
|
+
"stroke-linejoin": "round",
|
|
85
|
+
"stroke-width": "2"
|
|
86
|
+
}, [createElementVNode("path", { d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" }), createElementVNode("path", { d: "M14 2v5a1 1 0 0 0 1 1h5M10 9H8m8 4H8m8 4H8" })], -1)])]);
|
|
87
|
+
}
|
|
88
|
+
var file_text_default = markRaw({
|
|
89
|
+
name: "lucide-file-text",
|
|
90
|
+
render: render$3
|
|
91
|
+
});
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region ~icons/lucide/file-type
|
|
94
|
+
var _hoisted_1$2 = {
|
|
95
|
+
viewBox: "0 0 24 24",
|
|
96
|
+
width: "1.2em",
|
|
97
|
+
height: "1.2em"
|
|
98
|
+
};
|
|
99
|
+
function render$2(_ctx, _cache) {
|
|
100
|
+
return openBlock(), createElementBlock("svg", _hoisted_1$2, [..._cache[0] || (_cache[0] = [createElementVNode("g", {
|
|
101
|
+
fill: "none",
|
|
102
|
+
stroke: "currentColor",
|
|
103
|
+
"stroke-linecap": "round",
|
|
104
|
+
"stroke-linejoin": "round",
|
|
105
|
+
"stroke-width": "2"
|
|
106
|
+
}, [createElementVNode("path", { d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" }), createElementVNode("path", { d: "M14 2v5a1 1 0 0 0 1 1h5m-9 10h2m-1-6v6m-3-5v-.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v.5" })], -1)])]);
|
|
107
|
+
}
|
|
108
|
+
var file_type_default = markRaw({
|
|
109
|
+
name: "lucide-file-type",
|
|
110
|
+
render: render$2
|
|
111
|
+
});
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region ~icons/lucide/image-off
|
|
114
|
+
var _hoisted_1$1 = {
|
|
115
|
+
viewBox: "0 0 24 24",
|
|
116
|
+
width: "1.2em",
|
|
117
|
+
height: "1.2em"
|
|
118
|
+
};
|
|
119
|
+
function render$1(_ctx, _cache) {
|
|
120
|
+
return openBlock(), createElementBlock("svg", _hoisted_1$1, [..._cache[0] || (_cache[0] = [createElementVNode("g", {
|
|
121
|
+
fill: "none",
|
|
122
|
+
stroke: "currentColor",
|
|
123
|
+
"stroke-linecap": "round",
|
|
124
|
+
"stroke-linejoin": "round",
|
|
125
|
+
"stroke-width": "2"
|
|
126
|
+
}, [createElementVNode("path", { d: "m2 2l20 20M10.41 10.41a2 2 0 1 1-2.83-2.83m5.92 5.92L6 21m12-9l3 3" }), createElementVNode("path", { d: "M3.59 3.59A2 2 0 0 0 3 5v14a2 2 0 0 0 2 2h14c.55 0 1.052-.22 1.41-.59M21 15V5a2 2 0 0 0-2-2H9" })], -1)])]);
|
|
127
|
+
}
|
|
128
|
+
var image_off_default = markRaw({
|
|
129
|
+
name: "lucide-image-off",
|
|
130
|
+
render: render$1
|
|
131
|
+
});
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/components/GrFilePreview/fileKindOf.ts
|
|
134
|
+
/** Точные типы. Порядок не важен — совпадение полное. */
|
|
135
|
+
var EXACT = {
|
|
136
|
+
"application/pdf": "pdf",
|
|
137
|
+
"application/x-pdf": "pdf",
|
|
138
|
+
"application/msword": "document",
|
|
139
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "document",
|
|
140
|
+
"application/vnd.oasis.opendocument.text": "document",
|
|
141
|
+
"application/rtf": "document",
|
|
142
|
+
"application/vnd.ms-excel": "spreadsheet",
|
|
143
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "spreadsheet",
|
|
144
|
+
"application/vnd.oasis.opendocument.spreadsheet": "spreadsheet",
|
|
145
|
+
"text/csv": "spreadsheet",
|
|
146
|
+
"text/tab-separated-values": "spreadsheet",
|
|
147
|
+
"application/zip": "archive",
|
|
148
|
+
"application/x-zip-compressed": "archive",
|
|
149
|
+
"application/x-rar-compressed": "archive",
|
|
150
|
+
"application/vnd.rar": "archive",
|
|
151
|
+
"application/x-7z-compressed": "archive",
|
|
152
|
+
"application/x-tar": "archive",
|
|
153
|
+
"application/gzip": "archive",
|
|
154
|
+
"application/x-gzip": "archive"
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Приведение к сравнимому виду: тип приходит из БД и бывает с параметрами
|
|
158
|
+
* (`text/plain; charset=utf-8`) и в любом регистре.
|
|
159
|
+
*/
|
|
160
|
+
function normalize(mime) {
|
|
161
|
+
return mime.split(";")[0].trim().toLowerCase();
|
|
162
|
+
}
|
|
163
|
+
function fileKindOf(mime) {
|
|
164
|
+
if (!mime) return "unknown";
|
|
165
|
+
const type = normalize(mime);
|
|
166
|
+
if (!type) return "unknown";
|
|
167
|
+
if (type.startsWith("image/")) return "image";
|
|
168
|
+
const exact = EXACT[type];
|
|
169
|
+
if (exact) return exact;
|
|
170
|
+
if (type.startsWith("text/")) return "document";
|
|
171
|
+
return "unknown";
|
|
172
|
+
}
|
|
173
|
+
/** Картинку показываем, остальное — подписываем иконкой. */
|
|
174
|
+
function isPreviewableKind(kind) {
|
|
175
|
+
return kind === "image";
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/components/GrFilePreview/GrFilePreview.vue?vue&type=script&setup=true&lang.ts
|
|
179
|
+
var _hoisted_1 = [
|
|
180
|
+
"src",
|
|
181
|
+
"alt",
|
|
182
|
+
"loading"
|
|
183
|
+
];
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/components/GrFilePreview/GrFilePreview.vue
|
|
186
|
+
var GrFilePreview_default = /* @__PURE__ */ defineComponent({
|
|
187
|
+
__name: "GrFilePreview",
|
|
188
|
+
props: {
|
|
189
|
+
src: { default: void 0 },
|
|
190
|
+
mime: { default: void 0 },
|
|
191
|
+
name: { default: void 0 },
|
|
192
|
+
tileSize: { default: void 0 },
|
|
193
|
+
ratio: { default: void 0 },
|
|
194
|
+
href: { default: void 0 },
|
|
195
|
+
as: { default: void 0 },
|
|
196
|
+
clickable: {
|
|
197
|
+
type: Boolean,
|
|
198
|
+
default: false
|
|
199
|
+
},
|
|
200
|
+
loading: { default: void 0 },
|
|
201
|
+
ariaLabel: { default: void 0 }
|
|
202
|
+
},
|
|
203
|
+
emits: ["click"],
|
|
204
|
+
setup(__props, { emit: __emit }) {
|
|
205
|
+
const props = __props;
|
|
206
|
+
const emit = __emit;
|
|
207
|
+
const resolvedTileSize = useGrComponentProp("GrFilePreview", "tileSize", () => props.tileSize, "md");
|
|
208
|
+
const resolvedRatio = useGrComponentProp("GrFilePreview", "ratio", () => props.ratio, "1:1");
|
|
209
|
+
const resolvedLoading = useGrComponentProp("GrFilePreview", "loading", () => props.loading, "lazy");
|
|
210
|
+
const kind = computed(() => fileKindOf(props.mime));
|
|
211
|
+
/**
|
|
212
|
+
* Состояний три, а не два. Сорвавшаяся загрузка деградирует в заглушку: превью
|
|
213
|
+
* может исчезнуть с диска, и «сломанная картинка» здесь не информативнее
|
|
214
|
+
* иконки. Отдельное `loading` нужно ленте вложений: два десятка плиток
|
|
215
|
+
* приезжают вразнобой, и без него сетка стоит дырами непонятной природы —
|
|
216
|
+
* «ещё едет» и «файл без превью» выглядят одинаково.
|
|
217
|
+
*/
|
|
218
|
+
const imageState = ref("loading");
|
|
219
|
+
watch(() => props.src, () => {
|
|
220
|
+
imageState.value = "loading";
|
|
221
|
+
});
|
|
222
|
+
const showImage = computed(() => isPreviewableKind(kind.value) && Boolean(props.src) && imageState.value !== "error");
|
|
223
|
+
const showSkeleton = computed(() => showImage.value && imageState.value === "loading");
|
|
224
|
+
/**
|
|
225
|
+
* Иконка вида. Картинка попадает сюда только сорвавшейся загрузкой — тогда и
|
|
226
|
+
* значок другой: «изображение не открылось», а не «это файл».
|
|
227
|
+
*/
|
|
228
|
+
const iconByKind = {
|
|
229
|
+
image: markRaw(image_off_default),
|
|
230
|
+
pdf: markRaw(file_text_default),
|
|
231
|
+
document: markRaw(file_type_default),
|
|
232
|
+
spreadsheet: markRaw(file_spreadsheet_default),
|
|
233
|
+
archive: markRaw(file_archive_default),
|
|
234
|
+
unknown: markRaw(file_default)
|
|
235
|
+
};
|
|
236
|
+
const fallbackIcon = computed(() => iconByKind[kind.value]);
|
|
237
|
+
const isInteractive = computed(() => Boolean(props.as) || Boolean(props.href) || props.clickable);
|
|
238
|
+
const rootTag = computed(() => {
|
|
239
|
+
if (props.as) return typeof props.as === "string" ? props.as : markRaw(props.as);
|
|
240
|
+
if (props.href) return "a";
|
|
241
|
+
return props.clickable ? "button" : "div";
|
|
242
|
+
});
|
|
243
|
+
/**
|
|
244
|
+
* Компонент-ссылка (`Link` от Inertia, `RouterLink`) рендерит `<a>` сам, и без
|
|
245
|
+
* `href` он ведёт в никуда. Строковый тег, кроме `a`, атрибут не понимает —
|
|
246
|
+
* там он и гасится.
|
|
247
|
+
*/
|
|
248
|
+
const rootHref = computed(() => typeof rootTag.value === "string" && rootTag.value !== "a" ? void 0 : props.href);
|
|
249
|
+
const rootClass = computed(() => [
|
|
250
|
+
filePreviewRootClass,
|
|
251
|
+
filePreviewRatioClass[resolvedRatio.value],
|
|
252
|
+
typeof resolvedTileSize.value === "number" ? "" : filePreviewTileWidths[resolvedTileSize.value],
|
|
253
|
+
isInteractive.value ? filePreviewInteractiveClass : ""
|
|
254
|
+
].filter(Boolean).join(" "));
|
|
255
|
+
const rootStyle = computed(() => typeof resolvedTileSize.value === "number" ? { width: `${resolvedTileSize.value}px` } : void 0);
|
|
256
|
+
/**
|
|
257
|
+
* `alt` не выдумывается. Есть имя — оно и есть `alt`; нет — картинка
|
|
258
|
+
* декоративна, и придуманное компонентом описание было бы хуже пустого.
|
|
259
|
+
*/
|
|
260
|
+
const alt = computed(() => props.name ?? "");
|
|
261
|
+
function onClick(event) {
|
|
262
|
+
emit("click", event);
|
|
263
|
+
}
|
|
264
|
+
return (_ctx, _cache) => {
|
|
265
|
+
return openBlock(), createBlock(resolveDynamicComponent(rootTag.value), {
|
|
266
|
+
"data-gr-file-preview": "",
|
|
267
|
+
"data-kind": kind.value,
|
|
268
|
+
type: rootTag.value === "button" ? "button" : void 0,
|
|
269
|
+
href: rootHref.value,
|
|
270
|
+
class: normalizeClass(rootClass.value),
|
|
271
|
+
style: normalizeStyle(rootStyle.value),
|
|
272
|
+
"aria-label": __props.ariaLabel,
|
|
273
|
+
onClick
|
|
274
|
+
}, {
|
|
275
|
+
default: withCtx(() => [showSkeleton.value ? (openBlock(), createBlock(GrSkeleton_default, {
|
|
276
|
+
key: 0,
|
|
277
|
+
"data-gr-file-preview-skeleton": "",
|
|
278
|
+
width: "100%",
|
|
279
|
+
height: "100%",
|
|
280
|
+
rounded: "var(--gr-file-preview-radius, var(--gr-radius-md))"
|
|
281
|
+
})) : createCommentVNode("", true), showImage.value ? (openBlock(), createElementBlock("img", {
|
|
282
|
+
key: __props.src ?? "",
|
|
283
|
+
"data-gr-file-preview-image": "",
|
|
284
|
+
src: __props.src ?? void 0,
|
|
285
|
+
alt: alt.value,
|
|
286
|
+
loading: unref(resolvedLoading),
|
|
287
|
+
class: normalizeClass([unref(filePreviewMediaClass), showSkeleton.value ? unref(filePreviewMediaLoadingClass) : ""]),
|
|
288
|
+
onLoad: _cache[0] || (_cache[0] = ($event) => imageState.value = "loaded"),
|
|
289
|
+
onError: _cache[1] || (_cache[1] = ($event) => imageState.value = "error")
|
|
290
|
+
}, null, 42, _hoisted_1)) : (openBlock(), createElementBlock("span", {
|
|
291
|
+
key: 2,
|
|
292
|
+
"data-gr-file-preview-fallback": "",
|
|
293
|
+
class: normalizeClass(unref(filePreviewFallbackClass))
|
|
294
|
+
}, [(openBlock(), createBlock(resolveDynamicComponent(fallbackIcon.value), {
|
|
295
|
+
class: normalizeClass(unref(filePreviewIconClass)),
|
|
296
|
+
"aria-hidden": "true"
|
|
297
|
+
}, null, 8, ["class"])), __props.name ? (openBlock(), createElementBlock("span", {
|
|
298
|
+
key: 0,
|
|
299
|
+
"data-gr-file-preview-label": "",
|
|
300
|
+
class: normalizeClass(unref(filePreviewLabelClass))
|
|
301
|
+
}, toDisplayString(__props.name), 3)) : createCommentVNode("", true)], 2))]),
|
|
302
|
+
_: 1
|
|
303
|
+
}, 8, [
|
|
304
|
+
"data-kind",
|
|
305
|
+
"type",
|
|
306
|
+
"href",
|
|
307
|
+
"class",
|
|
308
|
+
"style",
|
|
309
|
+
"aria-label"
|
|
310
|
+
]);
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
//#endregion
|
|
315
|
+
export { GrFilePreview_default as t };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GrFilePreview-rbE3zL9w.js","names":[],"sources":["../../../../src/components/GrFilePreview/fileKindOf.ts","../../../../src/components/GrFilePreview/GrFilePreview.vue","../../../../src/components/GrFilePreview/GrFilePreview.vue"],"sourcesContent":["/**\n * Вид файла по MIME-типу.\n *\n * Тип решает **`mime`, а не расширение**: расширение врёт (`.dat` у выгрузки,\n * `.pdf` у переименованного архива), а бэкенд отдаёт настоящий тип — брать надо\n * его. От вида зависит единственное решение компонента: рисовать `<img>` или\n * иконку.\n */\n\nexport type GrFileKind =\n | 'image'\n | 'pdf'\n | 'document'\n | 'spreadsheet'\n | 'archive'\n /** Тип не опознан, пуст или это `application/octet-stream`. */\n | 'unknown'\n\n/** Точные типы. Порядок не важен — совпадение полное. */\nconst EXACT: Record<string, GrFileKind> = {\n 'application/pdf': 'pdf',\n 'application/x-pdf': 'pdf',\n\n 'application/msword': 'document',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'document',\n 'application/vnd.oasis.opendocument.text': 'document',\n 'application/rtf': 'document',\n\n 'application/vnd.ms-excel': 'spreadsheet',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'spreadsheet',\n 'application/vnd.oasis.opendocument.spreadsheet': 'spreadsheet',\n 'text/csv': 'spreadsheet',\n 'text/tab-separated-values': 'spreadsheet',\n\n 'application/zip': 'archive',\n 'application/x-zip-compressed': 'archive',\n 'application/x-rar-compressed': 'archive',\n 'application/vnd.rar': 'archive',\n 'application/x-7z-compressed': 'archive',\n 'application/x-tar': 'archive',\n 'application/gzip': 'archive',\n 'application/x-gzip': 'archive',\n}\n\n/**\n * Приведение к сравнимому виду: тип приходит из БД и бывает с параметрами\n * (`text/plain; charset=utf-8`) и в любом регистре.\n */\nfunction normalize(mime: string): string {\n return mime.split(';')[0].trim().toLowerCase()\n}\n\nexport function fileKindOf(mime: string | null | undefined): GrFileKind {\n if (!mime) return 'unknown'\n\n const type = normalize(mime)\n if (!type) return 'unknown'\n\n // Картинка — единственный вид, который компонент рисует, а не подписывает.\n if (type.startsWith('image/')) return 'image'\n\n const exact = EXACT[type]\n if (exact) return exact\n\n // `text/csv` уже разобран выше: таблица важнее общего правила для текста.\n if (type.startsWith('text/')) return 'document'\n\n return 'unknown'\n}\n\n/** Картинку показываем, остальное — подписываем иконкой. */\nexport function isPreviewableKind(kind: GrFileKind): boolean {\n return kind === 'image'\n}\n","<script setup lang=\"ts\">\nimport { computed, markRaw, ref, watch, type Component } from 'vue'\n\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\n\nimport IconFile from '~icons/lucide/file'\nimport IconFileArchive from '~icons/lucide/file-archive'\nimport IconFileSpreadsheet from '~icons/lucide/file-spreadsheet'\nimport IconFileText from '~icons/lucide/file-text'\nimport IconFileType from '~icons/lucide/file-type'\nimport IconImageOff from '~icons/lucide/image-off'\n\nimport type { GrSizeWithPx } from '../shared/sizes'\n\nimport { fileKindOf, isPreviewableKind, type GrFileKind } from './fileKindOf'\nimport {\n filePreviewFallbackClass,\n filePreviewIconClass,\n filePreviewInteractiveClass,\n filePreviewLabelClass,\n filePreviewMediaClass,\n filePreviewMediaLoadingClass,\n filePreviewRatioClass,\n filePreviewRootClass,\n filePreviewTileWidths,\n type GrFilePreviewRatio,\n} from './grFilePreviewStyles'\n\nexport type { GrFileKind } from './fileKindOf'\nexport type { GrFilePreviewRatio } from './grFilePreviewStyles'\n\n/**\n * Плитка превью файла с фолбэком по типу.\n *\n * Показывает **сохранённый** файл, а не отправляет новый: отправка — это\n * `GrFormFile` и `GrFileUpload`. Просмотрщик во весь экран открывает\n * потребитель по событию `click`.\n */\nexport interface GrFilePreviewProps {\n /** Адрес превью. Пусто — сразу заглушка по типу. */\n src?: string | null\n /** MIME-тип. Он решает, картинка это или файл. */\n mime?: string | null\n /** Имя файла: доступное имя картинки и подпись заглушки. */\n name?: string | null\n /**\n * Ступень канонической шкалы либо произвольная ширина в пикселях. Число —\n * escape-hatch, как диаметр у GrAvatar: плитка 96px в ленте вложений в\n * четыре ступени не укладывается.\n */\n tileSize?: GrSizeWithPx\n ratio?: GrFilePreviewRatio\n /** Ссылка на оригинал — для не-картинок и для перехода мимо просмотрщика. */\n href?: string\n /** Свой корневой тег (`RouterLink`, `Link` от Inertia). Сильнее `href`. */\n as?: string | Component\n /** Плитка кликабельна и эмитит `click` — обычно чтобы открыть просмотрщик. */\n clickable?: boolean\n loading?: 'lazy' | 'eager'\n /**\n * Доступное имя интерактивной плитки. Не задано — имя приходит из содержимого:\n * `alt` картинки или подписи заглушки.\n */\n ariaLabel?: string\n}\n\nexport interface GrFilePreviewEmits {\n (e: 'click', event: MouseEvent): void\n}\n\nconst props = withDefaults(defineProps<GrFilePreviewProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем компонент\n // заглянет в `GrConfigProvider`.\n src: undefined,\n mime: undefined,\n name: undefined,\n tileSize: undefined,\n ratio: undefined,\n href: undefined,\n as: undefined,\n clickable: false,\n loading: undefined,\n ariaLabel: undefined,\n})\n\nconst emit = defineEmits<GrFilePreviewEmits>()\n\nconst resolvedTileSize = useGrComponentProp('GrFilePreview', 'tileSize', () => props.tileSize, 'md')\nconst resolvedRatio = useGrComponentProp('GrFilePreview', 'ratio', () => props.ratio, '1:1')\nconst resolvedLoading = useGrComponentProp('GrFilePreview', 'loading', () => props.loading, 'lazy')\n\nconst kind = computed<GrFileKind>(() => fileKindOf(props.mime))\n\n/**\n * Состояний три, а не два. Сорвавшаяся загрузка деградирует в заглушку: превью\n * может исчезнуть с диска, и «сломанная картинка» здесь не информативнее\n * иконки. Отдельное `loading` нужно ленте вложений: два десятка плиток\n * приезжают вразнобой, и без него сетка стоит дырами непонятной природы —\n * «ещё едет» и «файл без превью» выглядят одинаково.\n */\ntype ImageState = 'loading' | 'loaded' | 'error'\n\nconst imageState = ref<ImageState>('loading')\n\n// Новая ссылка не должна наследовать ни ошибку прошлой, ни её готовность.\nwatch(() => props.src, () => { imageState.value = 'loading' })\n\nconst showImage = computed(() => (\n isPreviewableKind(kind.value) && Boolean(props.src) && imageState.value !== 'error'\n))\n\nconst showSkeleton = computed(() => showImage.value && imageState.value === 'loading')\n\n/**\n * Иконка вида. Картинка попадает сюда только сорвавшейся загрузкой — тогда и\n * значок другой: «изображение не открылось», а не «это файл».\n */\nconst iconByKind: Record<GrFileKind, Component> = {\n image: markRaw(IconImageOff),\n pdf: markRaw(IconFileText),\n document: markRaw(IconFileType),\n spreadsheet: markRaw(IconFileSpreadsheet),\n archive: markRaw(IconFileArchive),\n unknown: markRaw(IconFile),\n}\n\nconst fallbackIcon = computed(() => iconByKind[kind.value])\n\nconst isInteractive = computed(() => Boolean(props.as) || Boolean(props.href) || props.clickable)\n\nconst rootTag = computed<string | Component>(() => {\n if (props.as) return typeof props.as === 'string' ? props.as : markRaw(props.as)\n if (props.href) return 'a'\n return props.clickable ? 'button' : 'div'\n})\n\n/**\n * Компонент-ссылка (`Link` от Inertia, `RouterLink`) рендерит `<a>` сам, и без\n * `href` он ведёт в никуда. Строковый тег, кроме `a`, атрибут не понимает —\n * там он и гасится.\n */\nconst rootHref = computed(() => (\n typeof rootTag.value === 'string' && rootTag.value !== 'a' ? undefined : props.href\n))\n\nconst rootClass = computed(() => [\n filePreviewRootClass,\n filePreviewRatioClass[resolvedRatio.value],\n typeof resolvedTileSize.value === 'number' ? '' : filePreviewTileWidths[resolvedTileSize.value],\n isInteractive.value ? filePreviewInteractiveClass : '',\n].filter(Boolean).join(' '))\n\nconst rootStyle = computed(() => (\n typeof resolvedTileSize.value === 'number' ? { width: `${resolvedTileSize.value}px` } : undefined\n))\n\n/**\n * `alt` не выдумывается. Есть имя — оно и есть `alt`; нет — картинка\n * декоративна, и придуманное компонентом описание было бы хуже пустого.\n */\nconst alt = computed(() => props.name ?? '')\n\nfunction onClick(event: MouseEvent): void {\n emit('click', event)\n}\n</script>\n\n<template>\n <component\n :is=\"rootTag\"\n data-gr-file-preview\n :data-kind=\"kind\"\n :type=\"rootTag === 'button' ? 'button' : undefined\"\n :href=\"rootHref\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n :aria-label=\"ariaLabel\"\n @click=\"onClick\"\n >\n <GrSkeleton\n v-if=\"showSkeleton\"\n data-gr-file-preview-skeleton\n width=\"100%\"\n height=\"100%\"\n rounded=\"var(--gr-file-preview-radius, var(--gr-radius-md))\"\n />\n\n <!--\n Условие — `showImage`, а не «картинка готова»: убери её из дерева на\n время загрузки, и браузер не начнёт качать, а `load` не придёт никогда.\n Поэтому она ждёт невидимой и вне потока — иначе поделила бы плитку со\n скелетом как второй flex-элемент.\n -->\n <img\n v-if=\"showImage\"\n :key=\"src ?? ''\"\n data-gr-file-preview-image\n :src=\"src ?? undefined\"\n :alt=\"alt\"\n :loading=\"resolvedLoading\"\n :class=\"[filePreviewMediaClass, showSkeleton ? filePreviewMediaLoadingClass : '']\"\n @load=\"imageState = 'loaded'\"\n @error=\"imageState = 'error'\"\n >\n\n <span v-else data-gr-file-preview-fallback :class=\"filePreviewFallbackClass\">\n <component :is=\"fallbackIcon\" :class=\"filePreviewIconClass\" aria-hidden=\"true\" />\n <span v-if=\"name\" data-gr-file-preview-label :class=\"filePreviewLabelClass\">{{ name }}</span>\n </span>\n </component>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, markRaw, ref, watch, type Component } from 'vue'\n\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\n\nimport IconFile from '~icons/lucide/file'\nimport IconFileArchive from '~icons/lucide/file-archive'\nimport IconFileSpreadsheet from '~icons/lucide/file-spreadsheet'\nimport IconFileText from '~icons/lucide/file-text'\nimport IconFileType from '~icons/lucide/file-type'\nimport IconImageOff from '~icons/lucide/image-off'\n\nimport type { GrSizeWithPx } from '../shared/sizes'\n\nimport { fileKindOf, isPreviewableKind, type GrFileKind } from './fileKindOf'\nimport {\n filePreviewFallbackClass,\n filePreviewIconClass,\n filePreviewInteractiveClass,\n filePreviewLabelClass,\n filePreviewMediaClass,\n filePreviewMediaLoadingClass,\n filePreviewRatioClass,\n filePreviewRootClass,\n filePreviewTileWidths,\n type GrFilePreviewRatio,\n} from './grFilePreviewStyles'\n\nexport type { GrFileKind } from './fileKindOf'\nexport type { GrFilePreviewRatio } from './grFilePreviewStyles'\n\n/**\n * Плитка превью файла с фолбэком по типу.\n *\n * Показывает **сохранённый** файл, а не отправляет новый: отправка — это\n * `GrFormFile` и `GrFileUpload`. Просмотрщик во весь экран открывает\n * потребитель по событию `click`.\n */\nexport interface GrFilePreviewProps {\n /** Адрес превью. Пусто — сразу заглушка по типу. */\n src?: string | null\n /** MIME-тип. Он решает, картинка это или файл. */\n mime?: string | null\n /** Имя файла: доступное имя картинки и подпись заглушки. */\n name?: string | null\n /**\n * Ступень канонической шкалы либо произвольная ширина в пикселях. Число —\n * escape-hatch, как диаметр у GrAvatar: плитка 96px в ленте вложений в\n * четыре ступени не укладывается.\n */\n tileSize?: GrSizeWithPx\n ratio?: GrFilePreviewRatio\n /** Ссылка на оригинал — для не-картинок и для перехода мимо просмотрщика. */\n href?: string\n /** Свой корневой тег (`RouterLink`, `Link` от Inertia). Сильнее `href`. */\n as?: string | Component\n /** Плитка кликабельна и эмитит `click` — обычно чтобы открыть просмотрщик. */\n clickable?: boolean\n loading?: 'lazy' | 'eager'\n /**\n * Доступное имя интерактивной плитки. Не задано — имя приходит из содержимого:\n * `alt` картинки или подписи заглушки.\n */\n ariaLabel?: string\n}\n\nexport interface GrFilePreviewEmits {\n (e: 'click', event: MouseEvent): void\n}\n\nconst props = withDefaults(defineProps<GrFilePreviewProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем компонент\n // заглянет в `GrConfigProvider`.\n src: undefined,\n mime: undefined,\n name: undefined,\n tileSize: undefined,\n ratio: undefined,\n href: undefined,\n as: undefined,\n clickable: false,\n loading: undefined,\n ariaLabel: undefined,\n})\n\nconst emit = defineEmits<GrFilePreviewEmits>()\n\nconst resolvedTileSize = useGrComponentProp('GrFilePreview', 'tileSize', () => props.tileSize, 'md')\nconst resolvedRatio = useGrComponentProp('GrFilePreview', 'ratio', () => props.ratio, '1:1')\nconst resolvedLoading = useGrComponentProp('GrFilePreview', 'loading', () => props.loading, 'lazy')\n\nconst kind = computed<GrFileKind>(() => fileKindOf(props.mime))\n\n/**\n * Состояний три, а не два. Сорвавшаяся загрузка деградирует в заглушку: превью\n * может исчезнуть с диска, и «сломанная картинка» здесь не информативнее\n * иконки. Отдельное `loading` нужно ленте вложений: два десятка плиток\n * приезжают вразнобой, и без него сетка стоит дырами непонятной природы —\n * «ещё едет» и «файл без превью» выглядят одинаково.\n */\ntype ImageState = 'loading' | 'loaded' | 'error'\n\nconst imageState = ref<ImageState>('loading')\n\n// Новая ссылка не должна наследовать ни ошибку прошлой, ни её готовность.\nwatch(() => props.src, () => { imageState.value = 'loading' })\n\nconst showImage = computed(() => (\n isPreviewableKind(kind.value) && Boolean(props.src) && imageState.value !== 'error'\n))\n\nconst showSkeleton = computed(() => showImage.value && imageState.value === 'loading')\n\n/**\n * Иконка вида. Картинка попадает сюда только сорвавшейся загрузкой — тогда и\n * значок другой: «изображение не открылось», а не «это файл».\n */\nconst iconByKind: Record<GrFileKind, Component> = {\n image: markRaw(IconImageOff),\n pdf: markRaw(IconFileText),\n document: markRaw(IconFileType),\n spreadsheet: markRaw(IconFileSpreadsheet),\n archive: markRaw(IconFileArchive),\n unknown: markRaw(IconFile),\n}\n\nconst fallbackIcon = computed(() => iconByKind[kind.value])\n\nconst isInteractive = computed(() => Boolean(props.as) || Boolean(props.href) || props.clickable)\n\nconst rootTag = computed<string | Component>(() => {\n if (props.as) return typeof props.as === 'string' ? props.as : markRaw(props.as)\n if (props.href) return 'a'\n return props.clickable ? 'button' : 'div'\n})\n\n/**\n * Компонент-ссылка (`Link` от Inertia, `RouterLink`) рендерит `<a>` сам, и без\n * `href` он ведёт в никуда. Строковый тег, кроме `a`, атрибут не понимает —\n * там он и гасится.\n */\nconst rootHref = computed(() => (\n typeof rootTag.value === 'string' && rootTag.value !== 'a' ? undefined : props.href\n))\n\nconst rootClass = computed(() => [\n filePreviewRootClass,\n filePreviewRatioClass[resolvedRatio.value],\n typeof resolvedTileSize.value === 'number' ? '' : filePreviewTileWidths[resolvedTileSize.value],\n isInteractive.value ? filePreviewInteractiveClass : '',\n].filter(Boolean).join(' '))\n\nconst rootStyle = computed(() => (\n typeof resolvedTileSize.value === 'number' ? { width: `${resolvedTileSize.value}px` } : undefined\n))\n\n/**\n * `alt` не выдумывается. Есть имя — оно и есть `alt`; нет — картинка\n * декоративна, и придуманное компонентом описание было бы хуже пустого.\n */\nconst alt = computed(() => props.name ?? '')\n\nfunction onClick(event: MouseEvent): void {\n emit('click', event)\n}\n</script>\n\n<template>\n <component\n :is=\"rootTag\"\n data-gr-file-preview\n :data-kind=\"kind\"\n :type=\"rootTag === 'button' ? 'button' : undefined\"\n :href=\"rootHref\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n :aria-label=\"ariaLabel\"\n @click=\"onClick\"\n >\n <GrSkeleton\n v-if=\"showSkeleton\"\n data-gr-file-preview-skeleton\n width=\"100%\"\n height=\"100%\"\n rounded=\"var(--gr-file-preview-radius, var(--gr-radius-md))\"\n />\n\n <!--\n Условие — `showImage`, а не «картинка готова»: убери её из дерева на\n время загрузки, и браузер не начнёт качать, а `load` не придёт никогда.\n Поэтому она ждёт невидимой и вне потока — иначе поделила бы плитку со\n скелетом как второй flex-элемент.\n -->\n <img\n v-if=\"showImage\"\n :key=\"src ?? ''\"\n data-gr-file-preview-image\n :src=\"src ?? undefined\"\n :alt=\"alt\"\n :loading=\"resolvedLoading\"\n :class=\"[filePreviewMediaClass, showSkeleton ? filePreviewMediaLoadingClass : '']\"\n @load=\"imageState = 'loaded'\"\n @error=\"imageState = 'error'\"\n >\n\n <span v-else data-gr-file-preview-fallback :class=\"filePreviewFallbackClass\">\n <component :is=\"fallbackIcon\" :class=\"filePreviewIconClass\" aria-hidden=\"true\" />\n <span v-if=\"name\" data-gr-file-preview-label :class=\"filePreviewLabelClass\">{{ name }}</span>\n </span>\n </component>\n</template>\n"],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { t as GrFilePreview_default } from "./chunks/GrFilePreview-rbE3zL9w.js";
|
|
2
|
+
import { n as grFilePreviewSafelist, t as grFilePreviewConfig } from "../../chunks/config-QJ54_fcM.js";
|
|
3
|
+
export { GrFilePreview_default as GrFilePreview, GrFilePreview_default as default, grFilePreviewConfig, grFilePreviewSafelist };
|
|
@@ -88,6 +88,12 @@ var GrForm_default = /* @__PURE__ */ defineComponent({
|
|
|
88
88
|
const props = __props;
|
|
89
89
|
const emit = __emit;
|
|
90
90
|
const { t } = useGranularityTranslations();
|
|
91
|
+
/**
|
|
92
|
+
* Модель наружу дженерик, а адресация полей внутри работает по строковому пути.
|
|
93
|
+
* Приведение безопасно: форма читает и пишет только по именам, объявленным
|
|
94
|
+
* полями (`GrFormField name`), а не по всему объекту.
|
|
95
|
+
*/
|
|
96
|
+
const modelRecord = computed(() => props.model);
|
|
91
97
|
const errors = ref({});
|
|
92
98
|
const fieldRegistry = /* @__PURE__ */ new Map();
|
|
93
99
|
/**
|
|
@@ -103,7 +109,7 @@ var GrForm_default = /* @__PURE__ */ defineComponent({
|
|
|
103
109
|
return toRuleArray(props.rules?.[name]);
|
|
104
110
|
}
|
|
105
111
|
function getValue(name) {
|
|
106
|
-
return getByPath(
|
|
112
|
+
return getByPath(modelRecord.value, name);
|
|
107
113
|
}
|
|
108
114
|
const requiredFields = computed(() => {
|
|
109
115
|
const set = new Set(selfRequiredFields.value);
|
|
@@ -142,7 +148,7 @@ var GrForm_default = /* @__PURE__ */ defineComponent({
|
|
|
142
148
|
async function runValidation(name, seq, rules) {
|
|
143
149
|
validatingSet.value = new Set(validatingSet.value).add(name);
|
|
144
150
|
try {
|
|
145
|
-
const message = await runFieldRules(getValue(name), rules,
|
|
151
|
+
const message = await runFieldRules(getValue(name), rules, modelRecord.value, resolveMessage);
|
|
146
152
|
if (validationSeq.get(name) !== seq) {
|
|
147
153
|
const current = validationRuns.get(name);
|
|
148
154
|
return current && current.seq !== seq ? current.promise : !errors.value[name];
|
|
@@ -195,7 +201,7 @@ var GrForm_default = /* @__PURE__ */ defineComponent({
|
|
|
195
201
|
for (const name of list) delete next[name];
|
|
196
202
|
errors.value = next;
|
|
197
203
|
}
|
|
198
|
-
const initialSnapshot = ref(cloneModelValue(
|
|
204
|
+
const initialSnapshot = ref(cloneModelValue(modelRecord.value));
|
|
199
205
|
/**
|
|
200
206
|
* Переснять «исходное» состояние. Нужен формам редактирования: модель там
|
|
201
207
|
* наполняется после ответа сервера, а снимок в `setup` фиксировал пустой
|
|
@@ -215,14 +221,15 @@ var GrForm_default = /* @__PURE__ */ defineComponent({
|
|
|
215
221
|
}
|
|
216
222
|
function resetFields(names) {
|
|
217
223
|
const snapshot = initialSnapshot.value;
|
|
224
|
+
const model = modelRecord.value;
|
|
218
225
|
const list = names ? Array.isArray(names) ? names : [names] : void 0;
|
|
219
|
-
const keys = list ?? [.../* @__PURE__ */ new Set([...Object.keys(
|
|
220
|
-
for (const key of keys) if (key in snapshot) setByPath(
|
|
221
|
-
else removeKey(
|
|
226
|
+
const keys = list ?? [.../* @__PURE__ */ new Set([...Object.keys(model), ...Object.keys(snapshot)])];
|
|
227
|
+
for (const key of keys) if (key in snapshot) setByPath(model, key, cloneModelValue(snapshot[key]));
|
|
228
|
+
else if (typeof model[key] !== "function") removeKey(model, key);
|
|
222
229
|
clearValidate(list);
|
|
223
230
|
}
|
|
224
231
|
/** Модель отличается от снимка. Файлы сравниваются отпечатком, а не ссылкой. */
|
|
225
|
-
const isDirty = computed(() => modelFingerprint(
|
|
232
|
+
const isDirty = computed(() => modelFingerprint(modelRecord.value) !== modelFingerprint(initialSnapshot.value));
|
|
226
233
|
/**
|
|
227
234
|
* Известных ошибок нет. Это не «валидация прошла»: до первого `validate()`
|
|
228
235
|
* ошибок нет просто потому, что никто не проверял.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GrForm-B_Qq_13k.js","names":[],"sources":["../../../../src/components/GrForm/modelSnapshot.ts","../../../../src/components/GrForm/GrForm.vue","../../../../src/components/GrForm/GrForm.vue"],"sourcesContent":["/**\n * Снимок модели формы: клон для `resetFields` и отпечаток для `isDirty`.\n *\n * Чистый модуль без Vue: обе операции читают «сквозь» reactive-прокси и\n * тестируются без монтирования.\n *\n * Почему не `JSON.parse(JSON.stringify(...))` и не `structuredClone`: первый\n * теряет `File` (у файла нет ни перечислимых собственных свойств, ни `toJSON`,\n * поэтому в снимок попадал `{}`), второй падает на прокси с `DataCloneError`.\n */\n\n/** `File` и прочие `Blob` — двоичные значения, для которых клон бессмыслен. */\nfunction isBinary(value: unknown): value is Blob {\n return typeof Blob !== 'undefined' && value instanceof Blob\n}\n\nfunction cloneValue(value: unknown): unknown {\n if (typeof value === 'function' || typeof value === 'symbol') return undefined\n if (value === null || typeof value !== 'object') return value\n\n // По ссылке: содержимое файла клонировать нечем, а идентичность как раз и\n // нужна — `resetFields` обязан вернуть тот самый файл, а не его подобие.\n if (isBinary(value)) return value\n\n if (Array.isArray(value)) {\n // Как у `JSON.stringify`: не-сериализуемый элемент массива становится `null`,\n // иначе сброс сдвинул бы индексы.\n return value.map((item) => {\n const cloned = cloneValue(item)\n return cloned === undefined ? null : cloned\n })\n }\n\n // `Date` и прочие носители `toJSON` ведут себя ровно как раньше.\n const toJSON = (value as { toJSON?: unknown }).toJSON\n if (typeof toJSON === 'function') return cloneValue((value as { toJSON: () => unknown }).toJSON())\n\n const out: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value)) {\n const cloned = cloneValue(item)\n if (cloned !== undefined) out[key] = cloned\n }\n return out\n}\n\n/** Клон значения модели: JSON-семантика плюс сохранённые по ссылке файлы. */\nexport function cloneModelValue<T>(value: T): T {\n return value === undefined ? value : (cloneValue(value) as T)\n}\n\n/**\n * Отпечаток файла для сравнения. Тождество здесь не годится: `resetFields`\n * кладёт в модель клон снимка, и сравнение по ссылке считало бы форму грязной\n * сразу после сброса. Тройки «имя, размер, время» достаточно, чтобы отличить\n * один выбранный файл от другого.\n */\nfunction binaryFingerprint(value: Blob): string {\n if (typeof File !== 'undefined' && value instanceof File)\n return `gr-file:${value.name}:${value.size}:${value.lastModified}`\n\n return `gr-blob:${value.size}:${value.type}`\n}\n\n/**\n * Строка, по которой сравниваются модель и снимок. Без неё любой `File`\n * сериализовался в `{}`, и «выбрал документ» было неотличимо от «не трогал».\n */\nexport function modelFingerprint(value: unknown): string {\n return JSON.stringify(value, (_key, item) => isBinary(item) ? binaryFingerprint(item) : item) ?? ''\n}\n","<script setup lang=\"ts\" generic=\"TModel extends object = Record<string, unknown>\">\nimport { computed, onBeforeUnmount, provide, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_KEY } from './context'\nimport { cloneModelValue, modelFingerprint } from './modelSnapshot'\nimport {\n getByPath,\n createGrFormMessageResolver,\n rulesForTrigger,\n rulesRequired,\n runFieldRules,\n setByPath,\n toRuleArray,\n type GrFormRule,\n type GrFormRules,\n type GrFormTrigger,\n} from './validation'\n\nexport type {\n GrFormFileRule,\n GrFormRule,\n GrFormRules,\n GrFormTrigger,\n GrFormValidatorResult,\n} from './validation'\n\n/**\n * Публичный GR-примитив «Form» — оркестрация валидации над `GrFormField`.\n *\n * Модель формы (`v-model`-объект приложения) + декларативные `rules` по имени поля.\n * Форма прогоняет правила, раскладывает ошибки по контексту, скроллит к первой\n * ошибке и эмитит `submit` только при валидной форме. Императивный API — через\n * template ref (`validate` / `validateField` / `clearValidate` / `resetFields` /\n * `scrollToField`).\n */\nexport interface GrFormProps<TModel extends object = Record<string, unknown>> {\n /**\n * Реактивный объект данных формы. Поля адресуются по `name` (`GrFormField`),\n * в т.ч. dot-path.\n *\n * Тип дженерик, а не `Record<string, unknown>`: моделью бывает объект чужой\n * библиотеки (`useForm` Inertia, стор), у которого рядом с полями лежат\n * собственные методы. Под словарь он не подходит, и потребителю пришлось бы\n * приводить его через `as unknown as` в каждой форме.\n */\n model: TModel\n /** Правила валидации по имени поля: `{ email: [{ required: true, type: 'email' }] }`. */\n rules?: GrFormRules\n /** Валидировать поле при потере фокуса. */\n validateOnBlur?: boolean\n /** Валидировать поле при каждом изменении значения. */\n validateOnChange?: boolean\n /** Скроллить к первому невалидному полю после `validate()`. */\n scrollToError?: boolean\n scrollBehavior?: ScrollBehavior\n /**\n * Выключить форму целиком — типично на время отправки. Доезжает до контролов\n * через контекст поля, поэтому обходить их по одному не нужно.\n */\n disabled?: boolean\n}\n\nexport interface GrFormEmits<TModel extends object = Record<string, unknown>> {\n /** Форма прошла валидацию по submit. Отдаёт `model`. */\n (e: 'submit', model: TModel): void\n /** Результат валидации одного поля. */\n (e: 'validate', name: string, valid: boolean, message: string | undefined): void\n /**\n * Submit не прошёл валидацию. Без этого события «форма невалидна» и «ничего\n * не произошло» выглядят для потребителя одинаково.\n */\n (e: 'invalid', errors: Record<string, string>): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormProps<TModel>>(),\n {\n rules: undefined,\n validateOnBlur: true,\n validateOnChange: false,\n scrollToError: true,\n scrollBehavior: 'smooth',\n disabled: false,\n },\n)\n\nconst emit = defineEmits<GrFormEmits<TModel>>()\n\nconst { t } = useGranularityTranslations()\n\n/**\n * Модель наружу дженерик, а адресация полей внутри работает по строковому пути.\n * Приведение безопасно: форма читает и пишет только по именам, объявленным\n * полями (`GrFormField name`), а не по всему объекту.\n */\nconst modelRecord = computed(() => props.model as Record<string, unknown>)\n\n// ————— Ошибки и реестр полей.\nconst errors = ref<Record<string, string | undefined>>({})\nconst fieldRegistry = new Map<string, () => HTMLElement | null>()\n\n/**\n * Поля, объявившие себя обязательными собственным пропом `required` — без\n * правила в `rules`. До этого они рисовали звёздочку, но submit пропускал их\n * пустыми: обязательность и валидация жили в разных местах.\n */\nconst selfRequiredFields = ref<Set<string>>(new Set())\n\n/** Поля с идущей асинхронной проверкой. */\nconst validatingSet = ref<Set<string>>(new Set())\nconst validatingFields = computed(() => validatingSet.value)\n\nfunction getRules(name: string): GrFormRule[] {\n return toRuleArray(props.rules?.[name])\n}\n\nfunction getValue(name: string): unknown {\n return getByPath(modelRecord.value, name)\n}\n\nconst requiredFields = computed(() => {\n const set = new Set<string>(selfRequiredFields.value)\n for (const name of Object.keys(props.rules ?? {})) {\n if (rulesRequired(getRules(name))) set.add(name)\n }\n return set\n})\n\n/** Все поля, которые форма обязана проверить: с правилами и обязательные без них. */\nconst validatedNames = computed(() => [\n ...new Set([...Object.keys(props.rules ?? {}), ...selfRequiredFields.value]),\n])\n\n/**\n * Правила поля с учётом неявной обязательности: `<GrFormField required>` без\n * записи в `rules` проверяется тем же `required`-правилом и тем же сообщением,\n * что и явное.\n */\nconst IMPLICIT_REQUIRED_RULE: GrFormRule = { required: true }\n\nfunction effectiveRules(name: string): GrFormRule[] {\n const rules = getRules(name)\n if (rules.length) return rules\n return selfRequiredFields.value.has(name) ? [IMPLICIT_REQUIRED_RULE] : []\n}\n\n// Дефолтные i18n-сообщения (перекрываются `rule.message`) — общие с любым\n// потребителем движка правил, не только с формой.\nconst resolveMessage = createGrFormMessageResolver(t)\n\n// ————— Валидация.\n// Счётчик поколений на имя поля: перекрывающиеся асинхронные прогоны пишут\n// результат в порядке РАЗРЕШЕНИЯ промисов, и без него медленный ответ про\n// старое значение затирал бы свежий (приём — `searchSeq` в GrAutocomplete).\nconst validationSeq = new Map<string, number>()\n// Последний стартовавший прогон поля: вытесненному нечего сказать о текущем\n// значении, и он отдаёт вердикт того, кто его вытеснил, — иначе `validate()`\n// собрал бы «валидно» из карты ошибок, куда новый прогон ещё не записал.\nconst validationRuns = new Map<string, { seq: number, promise: Promise<boolean> }>()\n\nasync function validateField(name: string, trigger?: GrFormTrigger): Promise<boolean> {\n if (trigger === 'blur' && !props.validateOnBlur) return !errors.value[name]\n\n const rules = rulesForTrigger(effectiveRules(name), trigger)\n if (!rules.length) return !errors.value[name]\n\n const seq = (validationSeq.get(name) ?? 0) + 1\n validationSeq.set(name, seq)\n\n const run = runValidation(name, seq, rules)\n validationRuns.set(name, { seq, promise: run })\n return run\n}\n\nasync function runValidation(name: string, seq: number, rules: GrFormRule[]): Promise<boolean> {\n // Признак «идёт проверка» нужен именно асинхронным правилам: без него поле\n // молчит до ответа сервера, и пользователь не знает, что что-то происходит.\n validatingSet.value = new Set(validatingSet.value).add(name)\n try {\n const message = await runFieldRules(getValue(name), rules, modelRecord.value, resolveMessage)\n\n // Устаревший прогон: его результат — про прежнее значение, не применять.\n if (validationSeq.get(name) !== seq) {\n const current = validationRuns.get(name)\n // Цепочка конечна: каждый следующий прогон новее предыдущего. Своего\n // прогона в реестре нет только после `clearValidate` — там проверять\n // нечего, форма уже чиста.\n return current && current.seq !== seq ? current.promise : !errors.value[name]\n }\n\n errors.value = { ...errors.value, [name]: message }\n emit('validate', name, !message, message)\n return !message\n }\n finally {\n if (validationSeq.get(name) === seq) {\n const next = new Set(validatingSet.value)\n next.delete(name)\n validatingSet.value = next\n }\n }\n}\n\nasync function validate(): Promise<boolean> {\n const results = await Promise.all(validatedNames.value.map(name => validateField(name)))\n const valid = results.every(Boolean)\n if (!valid && props.scrollToError) scrollToFirstError()\n return valid\n}\n\n/**\n * Отменить летящие проверки: их ответ относится к состоянию до сброса, и\n * записать его в очищенную форму значило бы вернуть пользователю ошибку,\n * которую он только что убрал. Заодно снимаем «проверяем» — ждать сервер,\n * чтобы погасить индикатор на сброшенном поле, не за чем.\n */\nfunction invalidateValidation(names?: string[]): void {\n const list = names ?? [...validationSeq.keys()]\n if (!list.length) return\n\n const nextValidating = new Set(validatingSet.value)\n for (const name of list) {\n validationSeq.set(name, (validationSeq.get(name) ?? 0) + 1)\n validationRuns.delete(name)\n nextValidating.delete(name)\n }\n validatingSet.value = nextValidating\n}\n\nfunction clearValidate(names?: string | string[]): void {\n if (!names) {\n invalidateValidation()\n errors.value = {}\n return\n }\n const list = Array.isArray(names) ? names : [names]\n invalidateValidation(list)\n const next = { ...errors.value }\n for (const name of list) delete next[name]\n errors.value = next\n}\n\nconst initialSnapshot = ref<Record<string, unknown>>(cloneModelValue(modelRecord.value))\n\n/**\n * Переснять «исходное» состояние. Нужен формам редактирования: модель там\n * наполняется после ответа сервера, а снимок в `setup` фиксировал пустой\n * объект — и `resetFields()` возвращал не «как было при загрузке», а пустоту.\n */\nfunction setSnapshot(model?: Record<string, unknown>): void {\n initialSnapshot.value = cloneModelValue((model ?? props.model) as Record<string, unknown>)\n}\n\n/**\n * Удаление ключа модели. Вынесено в функцию не ради красоты: модель — проп, и\n * прямое `delete props.model[key]` линтер справедливо считает мутацией пропа.\n * Мутируем мы при этом по контракту — `model` у формы и есть общий объект\n * данных приложения, ради которого форма и существует.\n */\nfunction removeKey(model: Record<string, unknown>, key: string): void {\n delete model[key]\n}\n\nfunction resetFields(names?: string | string[]): void {\n const snapshot = initialSnapshot.value\n const model = modelRecord.value\n const list = names ? (Array.isArray(names) ? names : [names]) : undefined\n\n // Ключи объединяем: поле, появившееся после снимка, надо **удалить**, а не\n // выставить в `undefined` — иначе «сброс» оставляет за собой мусор.\n const keys = list ?? [...new Set([...Object.keys(model), ...Object.keys(snapshot)])]\n\n // Метод не удаляем, даже если его нет в снимке. Снимок строится клоном, а клон\n // отбрасывает функции — то есть для внешней модели (`useForm` Inertia, стор)\n // «нет в снимке» означает «это её метод», и сброс снёс бы саму модель.\n for (const key of keys) {\n if (key in snapshot) setByPath(model, key, cloneModelValue(snapshot[key]))\n else if (typeof model[key] !== 'function') removeKey(model, key)\n }\n\n clearValidate(list)\n}\n\n/** Модель отличается от снимка. Файлы сравниваются отпечатком, а не ссылкой. */\nconst isDirty = computed(() => modelFingerprint(modelRecord.value) !== modelFingerprint(initialSnapshot.value))\n\n/**\n * Известных ошибок нет. Это не «валидация прошла»: до первого `validate()`\n * ошибок нет просто потому, что никто не проверял.\n */\nconst isValid = computed(() => Object.values(errors.value).every(message => !message))\n\n// ————— Scroll-to-error.\nfunction scrollToField(name: string): void {\n const el = fieldRegistry.get(name)?.()\n if (!el) return\n el.scrollIntoView({ behavior: props.scrollBehavior, block: 'center' })\n const focusable = el.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')\n focusable?.focus?.()\n}\n\nfunction scrollToFirstError(): void {\n const elements = [...fieldRegistry.entries()]\n .filter(([name]) => errors.value[name])\n .map(([, getEl]) => getEl())\n .filter((el): el is HTMLElement => Boolean(el))\n\n // Первое по порядку в DOM — «самое верхнее» невалидное поле на экране.\n elements.sort((a, b) =>\n (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1,\n )\n\n const first = elements[0]\n if (!first) return\n first.scrollIntoView({ behavior: props.scrollBehavior, block: 'center' })\n first.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')?.focus?.()\n}\n\n// ————— «Умный» change: чистим ошибку по мере исправления; валидируем при validateOnChange.\nlet stopWatchers: Array<() => void> = []\nwatch(\n // Ключ — строка имён, а не массив: идентичность массива меняется на каждый\n // пересчёт геттера, и все пер-полевые watcher'ы пересоздавались впустую.\n () => validatedNames.value.join(' '),\n () => {\n stopWatchers.forEach(stop => stop())\n stopWatchers = validatedNames.value.map(name =>\n watch(\n () => getValue(name),\n () => {\n if (props.validateOnChange) void validateField(name, 'change')\n else if (errors.value[name]) void validateField(name) // ре-валидация, чтобы снять ошибку\n },\n { deep: true },\n ),\n )\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => stopWatchers.forEach(stop => stop()))\n\n// ————— Контекст для `GrFormField`.\nprovide(GR_FORM_KEY, {\n errors,\n requiredFields,\n validatingFields,\n disabled: computed(() => props.disabled),\n hasField: (name: string) => effectiveRules(name).length > 0,\n registerField: (name, getEl, registration) => {\n fieldRegistry.set(name, getEl)\n\n const stopRequired = registration?.required\n ? watch(\n registration.required,\n (required) => {\n const next = new Set(selfRequiredFields.value)\n if (required) next.add(name)\n else next.delete(name)\n selfRequiredFields.value = next\n },\n { immediate: true },\n )\n : undefined\n\n return () => {\n fieldRegistry.delete(name)\n // Иначе счётчики поколений копятся на динамических именах\n // (`items.417.email`) до размонтирования формы.\n validationSeq.delete(name)\n validationRuns.delete(name)\n stopRequired?.()\n const next = new Set(selfRequiredFields.value)\n next.delete(name)\n selfRequiredFields.value = next\n }\n },\n validateField,\n})\n\nfunction onSubmit(): void {\n void validate().then((valid) => {\n if (valid) {\n emit('submit', props.model)\n return\n }\n\n const failed: Record<string, string> = {}\n for (const [name, message] of Object.entries(errors.value)) {\n if (message) failed[name] = message\n }\n emit('invalid', failed)\n })\n}\n\ndefineExpose({\n validate,\n validateField,\n clearValidate,\n resetFields,\n scrollToField,\n setSnapshot,\n isDirty,\n isValid,\n validatingFields,\n})\n</script>\n\n<template>\n <form data-gr-form novalidate @submit.prevent=\"onSubmit\">\n <slot\n :validate=\"validate\"\n :errors=\"errors\"\n :is-dirty=\"isDirty\"\n :is-valid=\"isValid\"\n :validating-fields=\"validatingFields\"\n :reset-fields=\"resetFields\"\n :set-snapshot=\"setSnapshot\"\n />\n </form>\n</template>\n","<script setup lang=\"ts\" generic=\"TModel extends object = Record<string, unknown>\">\nimport { computed, onBeforeUnmount, provide, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_KEY } from './context'\nimport { cloneModelValue, modelFingerprint } from './modelSnapshot'\nimport {\n getByPath,\n createGrFormMessageResolver,\n rulesForTrigger,\n rulesRequired,\n runFieldRules,\n setByPath,\n toRuleArray,\n type GrFormRule,\n type GrFormRules,\n type GrFormTrigger,\n} from './validation'\n\nexport type {\n GrFormFileRule,\n GrFormRule,\n GrFormRules,\n GrFormTrigger,\n GrFormValidatorResult,\n} from './validation'\n\n/**\n * Публичный GR-примитив «Form» — оркестрация валидации над `GrFormField`.\n *\n * Модель формы (`v-model`-объект приложения) + декларативные `rules` по имени поля.\n * Форма прогоняет правила, раскладывает ошибки по контексту, скроллит к первой\n * ошибке и эмитит `submit` только при валидной форме. Императивный API — через\n * template ref (`validate` / `validateField` / `clearValidate` / `resetFields` /\n * `scrollToField`).\n */\nexport interface GrFormProps<TModel extends object = Record<string, unknown>> {\n /**\n * Реактивный объект данных формы. Поля адресуются по `name` (`GrFormField`),\n * в т.ч. dot-path.\n *\n * Тип дженерик, а не `Record<string, unknown>`: моделью бывает объект чужой\n * библиотеки (`useForm` Inertia, стор), у которого рядом с полями лежат\n * собственные методы. Под словарь он не подходит, и потребителю пришлось бы\n * приводить его через `as unknown as` в каждой форме.\n */\n model: TModel\n /** Правила валидации по имени поля: `{ email: [{ required: true, type: 'email' }] }`. */\n rules?: GrFormRules\n /** Валидировать поле при потере фокуса. */\n validateOnBlur?: boolean\n /** Валидировать поле при каждом изменении значения. */\n validateOnChange?: boolean\n /** Скроллить к первому невалидному полю после `validate()`. */\n scrollToError?: boolean\n scrollBehavior?: ScrollBehavior\n /**\n * Выключить форму целиком — типично на время отправки. Доезжает до контролов\n * через контекст поля, поэтому обходить их по одному не нужно.\n */\n disabled?: boolean\n}\n\nexport interface GrFormEmits<TModel extends object = Record<string, unknown>> {\n /** Форма прошла валидацию по submit. Отдаёт `model`. */\n (e: 'submit', model: TModel): void\n /** Результат валидации одного поля. */\n (e: 'validate', name: string, valid: boolean, message: string | undefined): void\n /**\n * Submit не прошёл валидацию. Без этого события «форма невалидна» и «ничего\n * не произошло» выглядят для потребителя одинаково.\n */\n (e: 'invalid', errors: Record<string, string>): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormProps<TModel>>(),\n {\n rules: undefined,\n validateOnBlur: true,\n validateOnChange: false,\n scrollToError: true,\n scrollBehavior: 'smooth',\n disabled: false,\n },\n)\n\nconst emit = defineEmits<GrFormEmits<TModel>>()\n\nconst { t } = useGranularityTranslations()\n\n/**\n * Модель наружу дженерик, а адресация полей внутри работает по строковому пути.\n * Приведение безопасно: форма читает и пишет только по именам, объявленным\n * полями (`GrFormField name`), а не по всему объекту.\n */\nconst modelRecord = computed(() => props.model as Record<string, unknown>)\n\n// ————— Ошибки и реестр полей.\nconst errors = ref<Record<string, string | undefined>>({})\nconst fieldRegistry = new Map<string, () => HTMLElement | null>()\n\n/**\n * Поля, объявившие себя обязательными собственным пропом `required` — без\n * правила в `rules`. До этого они рисовали звёздочку, но submit пропускал их\n * пустыми: обязательность и валидация жили в разных местах.\n */\nconst selfRequiredFields = ref<Set<string>>(new Set())\n\n/** Поля с идущей асинхронной проверкой. */\nconst validatingSet = ref<Set<string>>(new Set())\nconst validatingFields = computed(() => validatingSet.value)\n\nfunction getRules(name: string): GrFormRule[] {\n return toRuleArray(props.rules?.[name])\n}\n\nfunction getValue(name: string): unknown {\n return getByPath(modelRecord.value, name)\n}\n\nconst requiredFields = computed(() => {\n const set = new Set<string>(selfRequiredFields.value)\n for (const name of Object.keys(props.rules ?? {})) {\n if (rulesRequired(getRules(name))) set.add(name)\n }\n return set\n})\n\n/** Все поля, которые форма обязана проверить: с правилами и обязательные без них. */\nconst validatedNames = computed(() => [\n ...new Set([...Object.keys(props.rules ?? {}), ...selfRequiredFields.value]),\n])\n\n/**\n * Правила поля с учётом неявной обязательности: `<GrFormField required>` без\n * записи в `rules` проверяется тем же `required`-правилом и тем же сообщением,\n * что и явное.\n */\nconst IMPLICIT_REQUIRED_RULE: GrFormRule = { required: true }\n\nfunction effectiveRules(name: string): GrFormRule[] {\n const rules = getRules(name)\n if (rules.length) return rules\n return selfRequiredFields.value.has(name) ? [IMPLICIT_REQUIRED_RULE] : []\n}\n\n// Дефолтные i18n-сообщения (перекрываются `rule.message`) — общие с любым\n// потребителем движка правил, не только с формой.\nconst resolveMessage = createGrFormMessageResolver(t)\n\n// ————— Валидация.\n// Счётчик поколений на имя поля: перекрывающиеся асинхронные прогоны пишут\n// результат в порядке РАЗРЕШЕНИЯ промисов, и без него медленный ответ про\n// старое значение затирал бы свежий (приём — `searchSeq` в GrAutocomplete).\nconst validationSeq = new Map<string, number>()\n// Последний стартовавший прогон поля: вытесненному нечего сказать о текущем\n// значении, и он отдаёт вердикт того, кто его вытеснил, — иначе `validate()`\n// собрал бы «валидно» из карты ошибок, куда новый прогон ещё не записал.\nconst validationRuns = new Map<string, { seq: number, promise: Promise<boolean> }>()\n\nasync function validateField(name: string, trigger?: GrFormTrigger): Promise<boolean> {\n if (trigger === 'blur' && !props.validateOnBlur) return !errors.value[name]\n\n const rules = rulesForTrigger(effectiveRules(name), trigger)\n if (!rules.length) return !errors.value[name]\n\n const seq = (validationSeq.get(name) ?? 0) + 1\n validationSeq.set(name, seq)\n\n const run = runValidation(name, seq, rules)\n validationRuns.set(name, { seq, promise: run })\n return run\n}\n\nasync function runValidation(name: string, seq: number, rules: GrFormRule[]): Promise<boolean> {\n // Признак «идёт проверка» нужен именно асинхронным правилам: без него поле\n // молчит до ответа сервера, и пользователь не знает, что что-то происходит.\n validatingSet.value = new Set(validatingSet.value).add(name)\n try {\n const message = await runFieldRules(getValue(name), rules, modelRecord.value, resolveMessage)\n\n // Устаревший прогон: его результат — про прежнее значение, не применять.\n if (validationSeq.get(name) !== seq) {\n const current = validationRuns.get(name)\n // Цепочка конечна: каждый следующий прогон новее предыдущего. Своего\n // прогона в реестре нет только после `clearValidate` — там проверять\n // нечего, форма уже чиста.\n return current && current.seq !== seq ? current.promise : !errors.value[name]\n }\n\n errors.value = { ...errors.value, [name]: message }\n emit('validate', name, !message, message)\n return !message\n }\n finally {\n if (validationSeq.get(name) === seq) {\n const next = new Set(validatingSet.value)\n next.delete(name)\n validatingSet.value = next\n }\n }\n}\n\nasync function validate(): Promise<boolean> {\n const results = await Promise.all(validatedNames.value.map(name => validateField(name)))\n const valid = results.every(Boolean)\n if (!valid && props.scrollToError) scrollToFirstError()\n return valid\n}\n\n/**\n * Отменить летящие проверки: их ответ относится к состоянию до сброса, и\n * записать его в очищенную форму значило бы вернуть пользователю ошибку,\n * которую он только что убрал. Заодно снимаем «проверяем» — ждать сервер,\n * чтобы погасить индикатор на сброшенном поле, не за чем.\n */\nfunction invalidateValidation(names?: string[]): void {\n const list = names ?? [...validationSeq.keys()]\n if (!list.length) return\n\n const nextValidating = new Set(validatingSet.value)\n for (const name of list) {\n validationSeq.set(name, (validationSeq.get(name) ?? 0) + 1)\n validationRuns.delete(name)\n nextValidating.delete(name)\n }\n validatingSet.value = nextValidating\n}\n\nfunction clearValidate(names?: string | string[]): void {\n if (!names) {\n invalidateValidation()\n errors.value = {}\n return\n }\n const list = Array.isArray(names) ? names : [names]\n invalidateValidation(list)\n const next = { ...errors.value }\n for (const name of list) delete next[name]\n errors.value = next\n}\n\nconst initialSnapshot = ref<Record<string, unknown>>(cloneModelValue(modelRecord.value))\n\n/**\n * Переснять «исходное» состояние. Нужен формам редактирования: модель там\n * наполняется после ответа сервера, а снимок в `setup` фиксировал пустой\n * объект — и `resetFields()` возвращал не «как было при загрузке», а пустоту.\n */\nfunction setSnapshot(model?: Record<string, unknown>): void {\n initialSnapshot.value = cloneModelValue((model ?? props.model) as Record<string, unknown>)\n}\n\n/**\n * Удаление ключа модели. Вынесено в функцию не ради красоты: модель — проп, и\n * прямое `delete props.model[key]` линтер справедливо считает мутацией пропа.\n * Мутируем мы при этом по контракту — `model` у формы и есть общий объект\n * данных приложения, ради которого форма и существует.\n */\nfunction removeKey(model: Record<string, unknown>, key: string): void {\n delete model[key]\n}\n\nfunction resetFields(names?: string | string[]): void {\n const snapshot = initialSnapshot.value\n const model = modelRecord.value\n const list = names ? (Array.isArray(names) ? names : [names]) : undefined\n\n // Ключи объединяем: поле, появившееся после снимка, надо **удалить**, а не\n // выставить в `undefined` — иначе «сброс» оставляет за собой мусор.\n const keys = list ?? [...new Set([...Object.keys(model), ...Object.keys(snapshot)])]\n\n // Метод не удаляем, даже если его нет в снимке. Снимок строится клоном, а клон\n // отбрасывает функции — то есть для внешней модели (`useForm` Inertia, стор)\n // «нет в снимке» означает «это её метод», и сброс снёс бы саму модель.\n for (const key of keys) {\n if (key in snapshot) setByPath(model, key, cloneModelValue(snapshot[key]))\n else if (typeof model[key] !== 'function') removeKey(model, key)\n }\n\n clearValidate(list)\n}\n\n/** Модель отличается от снимка. Файлы сравниваются отпечатком, а не ссылкой. */\nconst isDirty = computed(() => modelFingerprint(modelRecord.value) !== modelFingerprint(initialSnapshot.value))\n\n/**\n * Известных ошибок нет. Это не «валидация прошла»: до первого `validate()`\n * ошибок нет просто потому, что никто не проверял.\n */\nconst isValid = computed(() => Object.values(errors.value).every(message => !message))\n\n// ————— Scroll-to-error.\nfunction scrollToField(name: string): void {\n const el = fieldRegistry.get(name)?.()\n if (!el) return\n el.scrollIntoView({ behavior: props.scrollBehavior, block: 'center' })\n const focusable = el.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')\n focusable?.focus?.()\n}\n\nfunction scrollToFirstError(): void {\n const elements = [...fieldRegistry.entries()]\n .filter(([name]) => errors.value[name])\n .map(([, getEl]) => getEl())\n .filter((el): el is HTMLElement => Boolean(el))\n\n // Первое по порядку в DOM — «самое верхнее» невалидное поле на экране.\n elements.sort((a, b) =>\n (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1,\n )\n\n const first = elements[0]\n if (!first) return\n first.scrollIntoView({ behavior: props.scrollBehavior, block: 'center' })\n first.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')?.focus?.()\n}\n\n// ————— «Умный» change: чистим ошибку по мере исправления; валидируем при validateOnChange.\nlet stopWatchers: Array<() => void> = []\nwatch(\n // Ключ — строка имён, а не массив: идентичность массива меняется на каждый\n // пересчёт геттера, и все пер-полевые watcher'ы пересоздавались впустую.\n () => validatedNames.value.join(' '),\n () => {\n stopWatchers.forEach(stop => stop())\n stopWatchers = validatedNames.value.map(name =>\n watch(\n () => getValue(name),\n () => {\n if (props.validateOnChange) void validateField(name, 'change')\n else if (errors.value[name]) void validateField(name) // ре-валидация, чтобы снять ошибку\n },\n { deep: true },\n ),\n )\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => stopWatchers.forEach(stop => stop()))\n\n// ————— Контекст для `GrFormField`.\nprovide(GR_FORM_KEY, {\n errors,\n requiredFields,\n validatingFields,\n disabled: computed(() => props.disabled),\n hasField: (name: string) => effectiveRules(name).length > 0,\n registerField: (name, getEl, registration) => {\n fieldRegistry.set(name, getEl)\n\n const stopRequired = registration?.required\n ? watch(\n registration.required,\n (required) => {\n const next = new Set(selfRequiredFields.value)\n if (required) next.add(name)\n else next.delete(name)\n selfRequiredFields.value = next\n },\n { immediate: true },\n )\n : undefined\n\n return () => {\n fieldRegistry.delete(name)\n // Иначе счётчики поколений копятся на динамических именах\n // (`items.417.email`) до размонтирования формы.\n validationSeq.delete(name)\n validationRuns.delete(name)\n stopRequired?.()\n const next = new Set(selfRequiredFields.value)\n next.delete(name)\n selfRequiredFields.value = next\n }\n },\n validateField,\n})\n\nfunction onSubmit(): void {\n void validate().then((valid) => {\n if (valid) {\n emit('submit', props.model)\n return\n }\n\n const failed: Record<string, string> = {}\n for (const [name, message] of Object.entries(errors.value)) {\n if (message) failed[name] = message\n }\n emit('invalid', failed)\n })\n}\n\ndefineExpose({\n validate,\n validateField,\n clearValidate,\n resetFields,\n scrollToField,\n setSnapshot,\n isDirty,\n isValid,\n validatingFields,\n})\n</script>\n\n<template>\n <form data-gr-form novalidate @submit.prevent=\"onSubmit\">\n <slot\n :validate=\"validate\"\n :errors=\"errors\"\n :is-dirty=\"isDirty\"\n :is-valid=\"isValid\"\n :validating-fields=\"validatingFields\"\n :reset-fields=\"resetFields\"\n :set-snapshot=\"setSnapshot\"\n />\n </form>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;AAYA,SAAS,SAAS,OAA+B;CAC/C,OAAO,OAAO,SAAS,eAAe,iBAAiB;AACzD;AAEA,SAAS,WAAW,OAAyB;CAC3C,IAAI,OAAO,UAAU,cAAc,OAAO,UAAU,UAAU,OAAO,KAAA;CACrE,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CAIxD,IAAI,SAAS,KAAK,GAAG,OAAO;CAE5B,IAAI,MAAM,QAAQ,KAAK,GAGrB,OAAO,MAAM,KAAK,SAAS;EACzB,MAAM,SAAS,WAAW,IAAI;EAC9B,OAAO,WAAW,KAAA,IAAY,OAAO;CACvC,CAAC;CAKH,IAAI,OADY,MAA+B,WACzB,YAAY,OAAO,WAAY,MAAoC,OAAO,CAAC;CAEjG,MAAM,MAA+B,CAAC;CACtC,KAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,KAAK,GAAG;EAC/C,MAAM,SAAS,WAAW,IAAI;EAC9B,IAAI,WAAW,KAAA,GAAW,IAAI,OAAO;CACvC;CACA,OAAO;AACT;;AAGA,SAAgB,gBAAmB,OAAa;CAC9C,OAAO,UAAU,KAAA,IAAY,QAAS,WAAW,KAAK;AACxD;;;;;;;AAQA,SAAS,kBAAkB,OAAqB;CAC9C,IAAI,OAAO,SAAS,eAAe,iBAAiB,MAClD,OAAO,WAAW,MAAM,KAAK,GAAG,MAAM,KAAK,GAAG,MAAM;CAEtD,OAAO,WAAW,MAAM,KAAK,GAAG,MAAM;AACxC;;;;;AAMA,SAAgB,iBAAiB,OAAwB;CACvD,OAAO,KAAK,UAAU,QAAQ,MAAM,SAAS,SAAS,IAAI,IAAI,kBAAkB,IAAI,IAAI,IAAI,KAAK;AACnG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECMA,MAAM,QAAQ;EAYd,MAAM,OAAO;EAEb,MAAM,EAAE,MAAM,2BAA2B;;;;;;EAOzC,MAAM,cAAc,eAAe,MAAM,KAAgC;EAGzE,MAAM,SAAS,IAAwC,CAAC,CAAC;EACzD,MAAM,gCAAgB,IAAI,IAAsC;;;;;;EAOhE,MAAM,qBAAqB,oBAAiB,IAAI,IAAI,CAAC;;EAGrD,MAAM,gBAAgB,oBAAiB,IAAI,IAAI,CAAC;EAChD,MAAM,mBAAmB,eAAe,cAAc,KAAK;EAE3D,SAAS,SAAS,MAA4B;GAC5C,OAAO,YAAY,MAAM,QAAQ,KAAK;EACxC;EAEA,SAAS,SAAS,MAAuB;GACvC,OAAO,UAAU,YAAY,OAAO,IAAI;EAC1C;EAEA,MAAM,iBAAiB,eAAe;GACpC,MAAM,MAAM,IAAI,IAAY,mBAAmB,KAAK;GACpD,KAAK,MAAM,QAAQ,OAAO,KAAK,MAAM,SAAS,CAAC,CAAC,GAC9C,IAAI,cAAc,SAAS,IAAI,CAAC,GAAG,IAAI,IAAI,IAAI;GAEjD,OAAO;EACT,CAAC;;EAGD,MAAM,iBAAiB,eAAe,CACpC,mBAAG,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,MAAM,SAAS,CAAC,CAAC,GAAG,GAAG,mBAAmB,KAAK,CAAC,CAC7E,CAAC;;;;;;EAOD,MAAM,yBAAqC,EAAE,UAAU,KAAK;EAE5D,SAAS,eAAe,MAA4B;GAClD,MAAM,QAAQ,SAAS,IAAI;GAC3B,IAAI,MAAM,QAAQ,OAAO;GACzB,OAAO,mBAAmB,MAAM,IAAI,IAAI,IAAI,CAAC,sBAAsB,IAAI,CAAC;EAC1E;EAIA,MAAM,iBAAiB,4BAA4B,CAAC;EAMpD,MAAM,gCAAgB,IAAI,IAAoB;EAI9C,MAAM,iCAAiB,IAAI,IAAwD;EAEnF,eAAe,cAAc,MAAc,SAA2C;GACpF,IAAI,YAAY,UAAU,CAAC,MAAM,gBAAgB,OAAO,CAAC,OAAO,MAAM;GAEtE,MAAM,QAAQ,gBAAgB,eAAe,IAAI,GAAG,OAAO;GAC3D,IAAI,CAAC,MAAM,QAAQ,OAAO,CAAC,OAAO,MAAM;GAExC,MAAM,OAAO,cAAc,IAAI,IAAI,KAAK,KAAK;GAC7C,cAAc,IAAI,MAAM,GAAG;GAE3B,MAAM,MAAM,cAAc,MAAM,KAAK,KAAK;GAC1C,eAAe,IAAI,MAAM;IAAE;IAAK,SAAS;GAAI,CAAC;GAC9C,OAAO;EACT;EAEA,eAAe,cAAc,MAAc,KAAa,OAAuC;GAG7F,cAAc,QAAQ,IAAI,IAAI,cAAc,KAAK,CAAC,CAAC,IAAI,IAAI;GAC3D,IAAI;IACF,MAAM,UAAU,MAAM,cAAc,SAAS,IAAI,GAAG,OAAO,YAAY,OAAO,cAAc;IAG5F,IAAI,cAAc,IAAI,IAAI,MAAM,KAAK;KACnC,MAAM,UAAU,eAAe,IAAI,IAAI;KAIvC,OAAO,WAAW,QAAQ,QAAQ,MAAM,QAAQ,UAAU,CAAC,OAAO,MAAM;IAC1E;IAEA,OAAO,QAAQ;KAAE,GAAG,OAAO;MAAQ,OAAO;IAAQ;IAClD,KAAK,YAAY,MAAM,CAAC,SAAS,OAAO;IACxC,OAAO,CAAC;GACV,UACQ;IACN,IAAI,cAAc,IAAI,IAAI,MAAM,KAAK;KACnC,MAAM,OAAO,IAAI,IAAI,cAAc,KAAK;KACxC,KAAK,OAAO,IAAI;KAChB,cAAc,QAAQ;IACxB;GACF;EACF;EAEA,eAAe,WAA6B;GAE1C,MAAM,SAAQ,MADQ,QAAQ,IAAI,eAAe,MAAM,KAAI,SAAQ,cAAc,IAAI,CAAC,CAAC,EAAA,CACjE,MAAM,OAAO;GACnC,IAAI,CAAC,SAAS,MAAM,eAAe,mBAAmB;GACtD,OAAO;EACT;;;;;;;EAQA,SAAS,qBAAqB,OAAwB;GACpD,MAAM,OAAO,SAAS,CAAC,GAAG,cAAc,KAAK,CAAC;GAC9C,IAAI,CAAC,KAAK,QAAQ;GAElB,MAAM,iBAAiB,IAAI,IAAI,cAAc,KAAK;GAClD,KAAK,MAAM,QAAQ,MAAM;IACvB,cAAc,IAAI,OAAO,cAAc,IAAI,IAAI,KAAK,KAAK,CAAC;IAC1D,eAAe,OAAO,IAAI;IAC1B,eAAe,OAAO,IAAI;GAC5B;GACA,cAAc,QAAQ;EACxB;EAEA,SAAS,cAAc,OAAiC;GACtD,IAAI,CAAC,OAAO;IACV,qBAAqB;IACrB,OAAO,QAAQ,CAAC;IAChB;GACF;GACA,MAAM,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;GAClD,qBAAqB,IAAI;GACzB,MAAM,OAAO,EAAE,GAAG,OAAO,MAAM;GAC/B,KAAK,MAAM,QAAQ,MAAM,OAAO,KAAK;GACrC,OAAO,QAAQ;EACjB;EAEA,MAAM,kBAAkB,IAA6B,gBAAgB,YAAY,KAAK,CAAC;;;;;;EAOvF,SAAS,YAAY,OAAuC;GAC1D,gBAAgB,QAAQ,gBAAiB,SAAS,MAAM,KAAiC;EAC3F;;;;;;;EAQA,SAAS,UAAU,OAAgC,KAAmB;GACpE,OAAO,MAAM;EACf;EAEA,SAAS,YAAY,OAAiC;GACpD,MAAM,WAAW,gBAAgB;GACjC,MAAM,QAAQ,YAAY;GAC1B,MAAM,OAAO,QAAS,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,IAAK,KAAA;GAIhE,MAAM,OAAO,QAAQ,CAAC,mBAAG,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,KAAK,GAAG,GAAG,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC;GAKnF,KAAK,MAAM,OAAO,MAChB,IAAI,OAAO,UAAU,UAAU,OAAO,KAAK,gBAAgB,SAAS,IAAI,CAAC;QACpE,IAAI,OAAO,MAAM,SAAS,YAAY,UAAU,OAAO,GAAG;GAGjE,cAAc,IAAI;EACpB;;EAGA,MAAM,UAAU,eAAe,iBAAiB,YAAY,KAAK,MAAM,iBAAiB,gBAAgB,KAAK,CAAC;;;;;EAM9G,MAAM,UAAU,eAAe,OAAO,OAAO,OAAO,KAAK,CAAC,CAAC,OAAM,YAAW,CAAC,OAAO,CAAC;EAGrF,SAAS,cAAc,MAAoB;GACzC,MAAM,KAAK,cAAc,IAAI,IAAI,CAAC,GAAG;GACrC,IAAI,CAAC,IAAI;GACT,GAAG,eAAe;IAAE,UAAU,MAAM;IAAgB,OAAO;GAAS,CAAC;GAErE,GADqB,cAA2B,oEAChD,CAAA,EAAW,QAAQ;EACrB;EAEA,SAAS,qBAA2B;GAClC,MAAM,WAAW,CAAC,GAAG,cAAc,QAAQ,CAAC,CAAA,CACzC,QAAQ,CAAC,UAAU,OAAO,MAAM,KAAK,CAAA,CACrC,KAAK,GAAG,WAAW,MAAM,CAAC,CAAA,CAC1B,QAAQ,OAA0B,QAAQ,EAAE,CAAC;GAGhD,SAAS,MAAM,GAAG,MACf,EAAE,wBAAwB,CAAC,IAAI,KAAK,8BAA+B,KAAK,CAC3E;GAEA,MAAM,QAAQ,SAAS;GACvB,IAAI,CAAC,OAAO;GACZ,MAAM,eAAe;IAAE,UAAU,MAAM;IAAgB,OAAO;GAAS,CAAC;GACxE,MAAM,cAA2B,oEAAkE,CAAC,EAAE,QAAQ;EAChH;EAGA,IAAI,eAAkC,CAAC;EACvC,YAGQ,eAAe,MAAM,KAAK,GAAG,SAC7B;GACJ,aAAa,SAAQ,SAAQ,KAAK,CAAC;GACnC,eAAe,eAAe,MAAM,KAAI,SACtC,YACQ,SAAS,IAAI,SACb;IACJ,IAAI,MAAM,kBAAkB,cAAmB,MAAM,QAAQ;SACxD,IAAI,OAAO,MAAM,OAAO,cAAmB,IAAI;GACtD,GACA,EAAE,MAAM,KAAK,CACf,CACF;EACF,GACA,EAAE,WAAW,KAAK,CACpB;EAEA,sBAAsB,aAAa,SAAQ,SAAQ,KAAK,CAAC,CAAC;EAG1D,QAAQ,aAAa;GACnB;GACA;GACA;GACA,UAAU,eAAe,MAAM,QAAQ;GACvC,WAAW,SAAiB,eAAe,IAAI,CAAC,CAAC,SAAS;GAC1D,gBAAgB,MAAM,OAAO,iBAAiB;IAC5C,cAAc,IAAI,MAAM,KAAK;IAE7B,MAAM,eAAe,cAAc,WAC/B,MACE,aAAa,WACZ,aAAa;KACZ,MAAM,OAAO,IAAI,IAAI,mBAAmB,KAAK;KAC7C,IAAI,UAAU,KAAK,IAAI,IAAI;UACtB,KAAK,OAAO,IAAI;KACrB,mBAAmB,QAAQ;IAC7B,GACA,EAAE,WAAW,KAAK,CACpB,IACA,KAAA;IAEJ,aAAa;KACX,cAAc,OAAO,IAAI;KAGzB,cAAc,OAAO,IAAI;KACzB,eAAe,OAAO,IAAI;KAC1B,eAAe;KACf,MAAM,OAAO,IAAI,IAAI,mBAAmB,KAAK;KAC7C,KAAK,OAAO,IAAI;KAChB,mBAAmB,QAAQ;IAC7B;GACF;GACA;EACF,CAAC;EAED,SAAS,WAAiB;GACxB,SAAc,CAAC,CAAC,MAAM,UAAU;IAC9B,IAAI,OAAO;KACT,KAAK,UAAU,MAAM,KAAK;KAC1B;IACF;IAEA,MAAM,SAAiC,CAAC;IACxC,KAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,OAAO,KAAK,GACvD,IAAI,SAAS,OAAO,QAAQ;IAE9B,KAAK,WAAW,MAAM;GACxB,CAAC;EACH;EAEA,SAAa;GACX;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF,CAAC;;uBAIC,mBAUO,QAAA;IAVD,gBAAA;IAAa,YAAA;IAAY,UAAM,cAAU,UAAQ,CAAA,SAAA,CAAA;OACrD,WAQE,KAAA,QAAA,WAAA;IAPW;IACV,QAAQ,OAAA;IACR,SAAU,QAAA;IACV,SAAU,QAAA;IACV,kBAAmB,iBAAA;IACL;IACA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as useGrFormContext } from "../../chunks/context-Bfrgsluh.js";
|
|
2
2
|
import { a as runFieldRules, t as createGrFormMessageResolver } from "../../chunks/validation-DauXRIkL.js";
|
|
3
|
-
import { t as GrForm_default } from "./chunks/GrForm-
|
|
3
|
+
import { t as GrForm_default } from "./chunks/GrForm-B_Qq_13k.js";
|
|
4
4
|
import { t as grFormConfig } from "../../chunks/config-Droqn55j.js";
|
|
5
5
|
export { GrForm_default as GrForm, GrForm_default as default, createGrFormMessageResolver, grFormConfig, runFieldRules, useGrFormContext };
|