@feugene/granularity 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/chunks/{en-BMjJZttK.js → en-QNZhjmSJ.js} +18 -1
  2. package/dist/chunks/en-QNZhjmSJ.js.map +1 -0
  3. package/dist/chunks/{es-EYCf8G_1.js → es-C1c5XsIK.js} +18 -1
  4. package/dist/chunks/es-C1c5XsIK.js.map +1 -0
  5. package/dist/chunks/{es-Cu_RlgIr.js → es-CKLPuE0h.js} +3 -3
  6. package/dist/chunks/{es-Cu_RlgIr.js.map → es-CKLPuE0h.js.map} +1 -1
  7. package/dist/chunks/granularityI18n-CXa_uN7f.js +46 -0
  8. package/dist/chunks/granularityI18n-CXa_uN7f.js.map +1 -0
  9. package/dist/chunks/{ru-DKswMSdd.js → ru-DcF7ciz0.js} +18 -1
  10. package/dist/chunks/ru-DcF7ciz0.js.map +1 -0
  11. package/dist/chunks/{shared-DO3Sr5FJ.js → shared-BkIIEolF.js} +7 -0
  12. package/dist/chunks/shared-BkIIEolF.js.map +1 -0
  13. package/dist/components/GrNavbar/chunks/{GrNavbar-BDIKAYfN.js → GrNavbar-BoVuSZ-V.js} +1 -1
  14. package/dist/components/GrNavbar/chunks/{GrNavbar-BDIKAYfN.js.map → GrNavbar-BoVuSZ-V.js.map} +1 -1
  15. package/dist/components/GrNavbar/index.js +1 -1
  16. package/dist/components/GrPagination/chunks/{GrPagination-BZlH-uEd.js → GrPagination-DW44c2-a.js} +1 -1
  17. package/dist/components/GrPagination/chunks/{GrPagination-BZlH-uEd.js.map → GrPagination-DW44c2-a.js.map} +1 -1
  18. package/dist/components/GrPagination/index.js +1 -1
  19. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-n0CZT4Ic.js → GrTreeSelect-DkAIksRW.js} +1 -1
  20. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-n0CZT4Ic.js.map → GrTreeSelect-DkAIksRW.js.map} +1 -1
  21. package/dist/components/GrTreeSelect/index.js +1 -1
  22. package/dist/granular-provider-node.js +1 -1
  23. package/dist/granular-provider.js +1 -1
  24. package/dist/i18n/all.js +1 -1
  25. package/dist/i18n/index.js +1 -1
  26. package/dist/index.js +977 -4
  27. package/dist/index.js.map +1 -1
  28. package/dist/types/src/components/GrResponseErrorBanner/GrFormErrorBanner.vue.d.ts +70 -0
  29. package/dist/types/src/components/GrResponseErrorBanner/GrResponseErrorBanner.vue.d.ts +96 -0
  30. package/dist/types/src/components/GrResponseErrorBanner/GrUploadErrorBanner.vue.d.ts +73 -0
  31. package/dist/types/src/components/GrResponseErrorBanner/config.d.ts +1 -0
  32. package/dist/types/src/components/GrResponseErrorBanner/index.d.ts +10 -0
  33. package/dist/types/src/components/GrResponseErrorBanner/parsers/abortErrorParser.d.ts +15 -0
  34. package/dist/types/src/components/GrResponseErrorBanner/parsers/createResponseErrorClassifier.d.ts +28 -0
  35. package/dist/types/src/components/GrResponseErrorBanner/parsers/fileValidationParser.d.ts +12 -0
  36. package/dist/types/src/components/GrResponseErrorBanner/parsers/httpStatusParser.d.ts +15 -0
  37. package/dist/types/src/components/GrResponseErrorBanner/parsers/index.d.ts +11 -0
  38. package/dist/types/src/components/GrResponseErrorBanner/parsers/jsonApiErrorParser.d.ts +17 -0
  39. package/dist/types/src/components/GrResponseErrorBanner/parsers/laravelValidationParser.d.ts +15 -0
  40. package/dist/types/src/components/GrResponseErrorBanner/parsers/networkErrorParser.d.ts +14 -0
  41. package/dist/types/src/components/GrResponseErrorBanner/parsers/normalizeError.d.ts +19 -0
  42. package/dist/types/src/components/GrResponseErrorBanner/parsers/plainMessageParser.d.ts +15 -0
  43. package/dist/types/src/components/GrResponseErrorBanner/parsers/problemDetailsParser.d.ts +17 -0
  44. package/dist/types/src/components/GrResponseErrorBanner/responseError.defaults.d.ts +18 -0
  45. package/dist/types/src/components/GrResponseErrorBanner/responseError.types.d.ts +115 -0
  46. package/dist/types/src/components/GrResponseErrorBanner/useResponseError.d.ts +62 -0
  47. package/dist/types/src/components/GrToaster/GrToaster.vue.d.ts +1 -1
  48. package/dist/types/src/granular-provider/shared.d.ts +1 -0
  49. package/dist/types/src/i18n/messages/en.d.ts +32 -0
  50. package/dist/types/src/i18n/messages/es.d.ts +32 -0
  51. package/dist/types/src/i18n/messages/ru.d.ts +32 -0
  52. package/dist/types/src/index.d.ts +1 -0
  53. package/package.json +1 -1
  54. package/dist/chunks/en-BMjJZttK.js.map +0 -1
  55. package/dist/chunks/es-EYCf8G_1.js.map +0 -1
  56. package/dist/chunks/granularityI18n-Dm4DuCS9.js +0 -26
  57. package/dist/chunks/granularityI18n-Dm4DuCS9.js.map +0 -1
  58. package/dist/chunks/ru-DKswMSdd.js.map +0 -1
  59. package/dist/chunks/shared-DO3Sr5FJ.js.map +0 -1
package/dist/index.js CHANGED
@@ -77,12 +77,13 @@ import { n as grLoadingSafelist, t as grLoadingConfig } from "./chunks/config-B4
77
77
  import "./components/GrLoading/index.js";
78
78
  import { n as grModalClassTokens, r as grModalSafelist, t as grModalConfig } from "./chunks/config-C5TN9J2Z.js";
79
79
  import "./components/GrModal/index.js";
80
- import { t as GrNavbar_default } from "./components/GrNavbar/chunks/GrNavbar-BDIKAYfN.js";
80
+ import { t as GrNavbar_default } from "./components/GrNavbar/chunks/GrNavbar-BoVuSZ-V.js";
81
+ import { t as useGranularityTranslations } from "./chunks/granularityI18n-CXa_uN7f.js";
81
82
  import { t as grNavbarConfig } from "./chunks/config-D1DI3roo.js";
82
83
  import { n as grNumberInputSafelist, t as grNumberInputConfig } from "./chunks/config-wo-ncgTH.js";
83
84
  import { t as GrNumberInput_default } from "./components/GrNumberInput/chunks/GrNumberInput-ECtFCIKV.js";
84
85
  import { t as GrSelect_default } from "./components/GrSelect/chunks/GrSelect-C3NAfTqL.js";
85
- import { t as GrPagination_default } from "./components/GrPagination/chunks/GrPagination-BZlH-uEd.js";
86
+ import { t as GrPagination_default } from "./components/GrPagination/chunks/GrPagination-DW44c2-a.js";
86
87
  import { t as grPaginationConfig } from "./chunks/config-DudL02ag.js";
87
88
  import { n as grProgressBarSafelist, t as grProgressBarConfig } from "./chunks/config-ZJVOVgqR.js";
88
89
  import "./components/GrProgressBar/index.js";
@@ -116,8 +117,980 @@ import { t as grTooltipConfig } from "./chunks/config-C0rHS1-l.js";
116
117
  import { t as GrTree_default } from "./components/GrTree/chunks/GrTree-Dlev9oBd.js";
117
118
  import { n as grTreeSafelist, t as grTreeConfig } from "./chunks/config-D32znLtV2.js";
118
119
  import { n as grTreeSelectSafelist, t as grTreeSelectConfig } from "./chunks/config-C0b_3LJV.js";
119
- import { t as GrTreeSelect_default } from "./components/GrTreeSelect/chunks/GrTreeSelect-n0CZT4Ic.js";
120
+ import { t as GrTreeSelect_default } from "./components/GrTreeSelect/chunks/GrTreeSelect-DkAIksRW.js";
120
121
  import { initThemeEarly, useTheme } from "./composables/useTheme.js";
122
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, openBlock, ref, renderList, shallowRef, toDisplayString, withCtx } from "vue";
123
+ //#region src/components/GrResponseErrorBanner/responseError.defaults.ts
124
+ /**
125
+ * Default English texts. Any field can be overridden via the `texts` prop
126
+ * on `GrResponseErrorBanner` or the `texts` option of `useResponseError`.
127
+ */
128
+ var DEFAULT_RESPONSE_ERROR_TEXTS = {
129
+ networkTitle: "No connection to the server",
130
+ networkMessage: "Check your internet connection and try again.",
131
+ abortedTitle: "Request cancelled",
132
+ abortedMessage: "The request was cancelled.",
133
+ validationTitle: "Check the data",
134
+ validationMessage: "The server returned a validation error. Fix the fields and try again.",
135
+ clientTitle: "Request error",
136
+ clientMessage: "The server rejected the request.",
137
+ serverTitle: "Server error",
138
+ serverMessage: "A server error occurred. Please try again.",
139
+ unknownTitle: "Something went wrong",
140
+ unknownMessage: "The operation could not be completed. Please try again.",
141
+ retryLabel: "Retry",
142
+ dismissLabel: "Dismiss"
143
+ };
144
+ /**
145
+ * Маппинг `kind` → `tone` по умолчанию. Применяется внутри баннера, если
146
+ * пользователь не передал `tone` или `toneByKind`. Цель — визуально отделить
147
+ * «исправь ввод» (warning) от «упало по сети/серверу» (danger).
148
+ */
149
+ var DEFAULT_TONE_BY_KIND = {
150
+ network: "danger",
151
+ aborted: "info",
152
+ validation: "warning",
153
+ client: "warning",
154
+ server: "danger",
155
+ unknown: "danger"
156
+ };
157
+ /**
158
+ * `kind`'ы, которые по умолчанию не показываются (баннер скрыт).
159
+ * Семантика: отмена пользователем — не «ошибка», UI обычно тихо проглатывает.
160
+ * Пользователь может переопределить через `autoHideKinds: []`, чтобы показать info-плашку.
161
+ */
162
+ var DEFAULT_AUTO_HIDE_KINDS = [];
163
+ //#endregion
164
+ //#region src/components/GrResponseErrorBanner/parsers/abortErrorParser.ts
165
+ /**
166
+ * Распознаёт отмену запроса клиентом.
167
+ *
168
+ * Триггеры:
169
+ * - `ctx.isAbort === true` (выставляется `normalizeError` для `AbortError`,
170
+ * axios `isCancel`/`ERR_CANCELED`, xhr `status === 0 && readyState === 4`).
171
+ *
172
+ * Возвращает `kind: 'aborted'` и `stop: true` — другие парсеры пропускаются,
173
+ * чтобы случайный `status === 0` не интерпретировался как сетевая ошибка.
174
+ *
175
+ * По умолчанию `aborted` показывается info-плашкой; чтобы скрыть совсем —
176
+ * добавьте `'aborted'` в `autoHideKinds`.
177
+ */
178
+ var abortErrorParser = (ctx) => {
179
+ if (!ctx.isAbort) return null;
180
+ return {
181
+ kind: "aborted",
182
+ message: ctx.texts.abortedMessage,
183
+ stop: true
184
+ };
185
+ };
186
+ //#endregion
187
+ //#region src/components/GrResponseErrorBanner/parsers/fileValidationParser.ts
188
+ /**
189
+ * Распознаёт `FileValidationError` от `GrFileUpload` (или совместимый по форме
190
+ * объект). Не зависит от жёсткого импорта класса — определяет по `name`.
191
+ *
192
+ * Активируется, если `ctx.raw.name === 'FileValidationError'` и есть массив
193
+ * `issues`/`errors` с полями `{ file?, code?, message }`.
194
+ *
195
+ * Возвращает `kind: 'validation'` и `stop: true` — это локальная клиентская
196
+ * валидация, незачем дальше идти по HTTP-парсерам.
197
+ */
198
+ var fileValidationParser = (ctx) => {
199
+ const raw = ctx.raw;
200
+ if (!raw || typeof raw !== "object") return null;
201
+ if (raw.name !== "FileValidationError") return null;
202
+ const list = raw.issues ?? raw.errors ?? [];
203
+ const details = [];
204
+ for (const it of list) {
205
+ if (!it || typeof it !== "object") continue;
206
+ const msg = typeof it.message === "string" ? it.message : "";
207
+ if (!msg) continue;
208
+ const fileName = it.file?.name;
209
+ details.push(fileName ? `${fileName}: ${msg}` : msg);
210
+ }
211
+ return {
212
+ kind: "validation",
213
+ message: raw.message || ctx.texts.validationMessage,
214
+ details: details.length ? details : void 0,
215
+ stop: true
216
+ };
217
+ };
218
+ //#endregion
219
+ //#region src/components/GrResponseErrorBanner/parsers/httpStatusParser.ts
220
+ /**
221
+ * Базовая классификация по HTTP-статусу.
222
+ *
223
+ * Правила:
224
+ * - `400` / `422` ⇒ `kind: 'validation'`,
225
+ * - остальные `4xx` ⇒ `kind: 'client'`,
226
+ * - `>= 500` ⇒ `kind: 'server'`.
227
+ *
228
+ * Не использует `stop` — даёт более специализированным парсерам
229
+ * (Laravel/JSON:API/RFC 7807) уточнить сообщение и `fieldErrors`.
230
+ *
231
+ * `message` берёт из `ctx.texts` как осмысленный фолбэк.
232
+ */
233
+ var httpStatusParser = (ctx) => {
234
+ const status = ctx.status;
235
+ if (typeof status !== "number") return null;
236
+ if (status >= 500) return {
237
+ kind: "server",
238
+ status,
239
+ message: ctx.texts.serverMessage
240
+ };
241
+ if (status === 400 || status === 422) return {
242
+ kind: "validation",
243
+ status,
244
+ message: ctx.texts.validationMessage
245
+ };
246
+ if (status >= 400) return {
247
+ kind: "client",
248
+ status,
249
+ message: ctx.texts.clientMessage
250
+ };
251
+ return null;
252
+ };
253
+ //#endregion
254
+ //#region src/components/GrResponseErrorBanner/parsers/jsonApiErrorParser.ts
255
+ /**
256
+ * Парсер JSON:API-стиля:
257
+ *
258
+ * ```json
259
+ * { "errors": [
260
+ * { "status": "422", "title": "Invalid Attribute",
261
+ * "detail": "Email is required.",
262
+ * "source": { "pointer": "/data/attributes/email" } }
263
+ * ] }
264
+ * ```
265
+ *
266
+ * Активируется, когда `ctx.body.errors` — массив объектов с `title`/`detail`.
267
+ * Поле извлекается из последнего сегмента `source.pointer`. Все `detail`
268
+ * собираются в `details`, первый `title` — в `message`.
269
+ */
270
+ var jsonApiErrorParser = (ctx) => {
271
+ const body = ctx.body;
272
+ if (!body || typeof body !== "object") return null;
273
+ const errors = body.errors;
274
+ if (!Array.isArray(errors) || errors.length === 0) return null;
275
+ const first = errors[0];
276
+ if (!first || typeof first !== "object") return null;
277
+ const has = (key) => key in first;
278
+ if (!has("title") && !has("detail") && !has("source")) return null;
279
+ const fieldErrorsMap = /* @__PURE__ */ new Map();
280
+ const details = [];
281
+ let message;
282
+ for (const e of errors) {
283
+ if (!e || typeof e !== "object") continue;
284
+ const eo = e;
285
+ const text = typeof eo.detail === "string" && eo.detail || typeof eo.title === "string" && eo.title || "";
286
+ if (!text) continue;
287
+ if (!message && typeof eo.title === "string") message = eo.title;
288
+ const pointer = typeof eo.source?.pointer === "string" ? eo.source.pointer : "";
289
+ const field = pointer ? pointer.split("/").filter(Boolean).pop() : void 0;
290
+ if (field) {
291
+ const list = fieldErrorsMap.get(field) ?? [];
292
+ list.push(text);
293
+ fieldErrorsMap.set(field, list);
294
+ }
295
+ details.push(text);
296
+ }
297
+ if (!details.length) return null;
298
+ const fieldErrors = Array.from(fieldErrorsMap, ([field, messages]) => ({
299
+ field,
300
+ messages
301
+ }));
302
+ const kind = fieldErrors.length ? "validation" : void 0;
303
+ return {
304
+ kind,
305
+ message: message || (kind === "validation" ? ctx.texts.validationMessage : ctx.texts.clientMessage),
306
+ details,
307
+ fieldErrors: fieldErrors.length ? fieldErrors : void 0
308
+ };
309
+ };
310
+ //#endregion
311
+ //#region src/components/GrResponseErrorBanner/parsers/laravelValidationParser.ts
312
+ /**
313
+ * Парсит Laravel-формат ответа об ошибке валидации:
314
+ *
315
+ * ```json
316
+ * { "message": "The given data was invalid.",
317
+ * "errors": { "file": ["The file is required."], "amount": ["..."] } }
318
+ * ```
319
+ *
320
+ * Активируется при наличии `ctx.body.errors` как plain-объекта (значения —
321
+ * массив строк или строка). Если есть `fieldErrors`, повышает `kind` до
322
+ * `validation` независимо от статуса (полезно, когда сервер прислал 200/4xx
323
+ * без 422). `message` берётся из `body.message` / `body.error`.
324
+ */
325
+ var laravelValidationParser = (ctx) => {
326
+ const body = ctx.body;
327
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
328
+ const data = body;
329
+ const errors = data.errors;
330
+ if (!errors || typeof errors !== "object" || Array.isArray(errors)) return null;
331
+ const fieldErrors = [];
332
+ for (const [field, raw] of Object.entries(errors)) {
333
+ const messages = Array.isArray(raw) ? raw.filter((m) => typeof m === "string") : typeof raw === "string" ? [raw] : [];
334
+ if (messages.length) fieldErrors.push({
335
+ field,
336
+ messages
337
+ });
338
+ }
339
+ if (!fieldErrors.length) return null;
340
+ return {
341
+ kind: "validation",
342
+ message: typeof data.message === "string" && data.message || typeof data.error === "string" && data.error || ctx.texts.validationMessage,
343
+ details: fieldErrors.flatMap((fe) => fe.messages),
344
+ fieldErrors
345
+ };
346
+ };
347
+ //#endregion
348
+ //#region src/components/GrResponseErrorBanner/parsers/networkErrorParser.ts
349
+ /**
350
+ * Распознаёт транспортные ошибки **без ответа сервера**: отсутствие интернета,
351
+ * DNS-фейл, CORS-preflight, fetch `TypeError('Failed to fetch')`,
352
+ * axios `ERR_NETWORK`/`ECONNABORTED`.
353
+ *
354
+ * Триггеры:
355
+ * - `ctx.isNetwork === true` (выставлено `normalizeError`),
356
+ * - либо нет ни `status`, ни `body`, ни признака отмены, но есть исходная
357
+ * ошибка с `code === 'ERR_NETWORK'` / `message ~ /network|fetch/i/`.
358
+ *
359
+ * Возвращает `kind: 'network'` и `stop: true`.
360
+ */
361
+ var networkErrorParser = (ctx) => {
362
+ if (ctx.isAbort) return null;
363
+ if (ctx.isNetwork) return {
364
+ kind: "network",
365
+ message: ctx.texts.networkMessage,
366
+ stop: true
367
+ };
368
+ if (typeof ctx.status !== "number" && ctx.body == null && ctx.raw && typeof ctx.raw === "object") {
369
+ const r = ctx.raw;
370
+ if (r.code === "ERR_NETWORK" || typeof r.message === "string" && /network|failed to fetch/i.test(r.message)) return {
371
+ kind: "network",
372
+ message: ctx.texts.networkMessage,
373
+ stop: true
374
+ };
375
+ }
376
+ return null;
377
+ };
378
+ //#endregion
379
+ //#region src/components/GrResponseErrorBanner/parsers/normalizeError.ts
380
+ /**
381
+ * Универсально приводит «сырой» вход к {@link NormalizedRawError}.
382
+ *
383
+ * Поддерживаемые источники:
384
+ * - **axios** — `AxiosError` с `response.status`/`response.data`/`response.headers`,
385
+ * либо `request` без `response` + `code === 'ERR_NETWORK'`, либо `code === 'ERR_CANCELED'`.
386
+ * - **fetch `Response`** — объект `Response` с `ok=false`. Тело читается через
387
+ * `clone().json()` (с фолбэком на `.text()`); поэтому функция асинхронная.
388
+ * - **fetch `TypeError`** — `TypeError('Failed to fetch')` ⇒ `isNetwork=true`.
389
+ * - **`{ response: Response, body: unknown }`** — если потребитель уже прочитал тело сам.
390
+ * - **`XMLHttpRequest`** / `{ status, responseText, response }` — пара полей.
391
+ * - **`AbortError` / `DOMException`** ⇒ `isAbort=true`.
392
+ * - **`Error`/строка/произвольный объект** — кладётся в `body`/`raw` как есть.
393
+ *
394
+ * Цель — отделить «откуда пришла ошибка» от «как её классифицировать»: парсеры
395
+ * работают только с нормализованным `ResponseErrorContext`, не зная о транспорте.
396
+ */
397
+ async function normalizeError(raw) {
398
+ if (isPlainNormalized(raw)) return {
399
+ ...raw,
400
+ raw
401
+ };
402
+ if (isResponseEnvelope(raw)) {
403
+ const res = raw.response;
404
+ return {
405
+ raw,
406
+ status: res.status,
407
+ body: raw.body,
408
+ headers: readHeaders(res)
409
+ };
410
+ }
411
+ if (isFetchResponse(raw)) {
412
+ const body = await readFetchBody(raw);
413
+ return {
414
+ raw,
415
+ status: raw.status,
416
+ body,
417
+ headers: readHeaders(raw)
418
+ };
419
+ }
420
+ if (isXhr(raw)) {
421
+ const body = parseMaybeJson(raw.responseText) ?? raw.response;
422
+ return {
423
+ raw,
424
+ status: typeof raw.status === "number" ? raw.status : void 0,
425
+ body,
426
+ isAbort: raw.status === 0 && raw.readyState === 4
427
+ };
428
+ }
429
+ if (isAxiosLike(raw)) {
430
+ const e = raw;
431
+ const status = e.response?.status;
432
+ const code = e.code;
433
+ const isAbort = code === "ERR_CANCELED" || code === "CANCELED" || e.__CANCEL__ === true;
434
+ const isNetwork = !e.response && (code === "ERR_NETWORK" || code === "ECONNABORTED");
435
+ return {
436
+ raw,
437
+ status,
438
+ body: e.response?.data,
439
+ headers: normalizeHeaderRecord(e.response?.headers),
440
+ isAbort,
441
+ isNetwork
442
+ };
443
+ }
444
+ if (isAbortError(raw)) return {
445
+ raw,
446
+ isAbort: true
447
+ };
448
+ if (raw instanceof TypeError && /fetch|network/i.test(raw.message || "")) return {
449
+ raw,
450
+ isNetwork: true
451
+ };
452
+ if (raw instanceof Error) return {
453
+ raw,
454
+ body: { message: raw.message }
455
+ };
456
+ if (typeof raw === "string") return {
457
+ raw,
458
+ body: { message: raw }
459
+ };
460
+ if (raw && typeof raw === "object") return {
461
+ raw,
462
+ body: raw
463
+ };
464
+ return { raw };
465
+ }
466
+ function isPlainNormalized(v) {
467
+ if (!v || typeof v !== "object") return false;
468
+ const o = v;
469
+ return "raw" in o && ("status" in o || "body" in o || "isAbort" in o || "isNetwork" in o || "headers" in o);
470
+ }
471
+ function isResponseEnvelope(v) {
472
+ return !!v && typeof v === "object" && "response" in v && isFetchResponse(v.response);
473
+ }
474
+ function isFetchResponse(v) {
475
+ return typeof Response !== "undefined" && v instanceof Response;
476
+ }
477
+ function isXhr(v) {
478
+ return typeof XMLHttpRequest !== "undefined" && v instanceof XMLHttpRequest;
479
+ }
480
+ function isAxiosLike(v) {
481
+ if (!v || typeof v !== "object") return false;
482
+ const o = v;
483
+ return o.isAxiosError === true || "response" in o && typeof o.response === "object" && o.response !== null || "code" in o && typeof o.code === "string";
484
+ }
485
+ function isAbortError(v) {
486
+ if (!v || typeof v !== "object") return false;
487
+ const o = v;
488
+ return o.name === "AbortError" || o.name === "CanceledError" || o.code === "ERR_CANCELED";
489
+ }
490
+ async function readFetchBody(res) {
491
+ if (res.bodyUsed) return void 0;
492
+ try {
493
+ const text = await res.clone().text();
494
+ return parseMaybeJson(text) ?? text;
495
+ } catch {
496
+ return;
497
+ }
498
+ }
499
+ function readHeaders(res) {
500
+ const out = {};
501
+ res.headers.forEach((value, key) => {
502
+ out[key.toLowerCase()] = value;
503
+ });
504
+ return out;
505
+ }
506
+ function normalizeHeaderRecord(h) {
507
+ if (!h || typeof h !== "object") return void 0;
508
+ const out = {};
509
+ for (const [k, v] of Object.entries(h)) if (typeof v === "string") out[k.toLowerCase()] = v;
510
+ return out;
511
+ }
512
+ function parseMaybeJson(text) {
513
+ if (typeof text !== "string" || !text) return void 0;
514
+ try {
515
+ return JSON.parse(text);
516
+ } catch {
517
+ return;
518
+ }
519
+ }
520
+ //#endregion
521
+ //#region src/components/GrResponseErrorBanner/parsers/plainMessageParser.ts
522
+ /**
523
+ * Финальный фолбэк-парсер. Достаёт хоть какое-нибудь человекочитаемое
524
+ * сообщение, чтобы баннер не был пустым.
525
+ *
526
+ * Источники (по приоритету):
527
+ * 1. `ctx.body` — если строка ⇒ message,
528
+ * 2. `ctx.body[messageKey]` (default `'message'`),
529
+ * 3. `ctx.body.error` (строка),
530
+ * 4. `ctx.raw.message` (для голых `Error`).
531
+ *
532
+ * Не выставляет `kind` — оставляет на усмотрение классификатора.
533
+ * Использует ключ `messageKey` из `ctx.meta._messageKey`, если задан.
534
+ */
535
+ var plainMessageParser = (ctx) => {
536
+ const messageKey = ctx.meta._messageKey || "message";
537
+ if (typeof ctx.body === "string" && ctx.body.trim()) return { message: ctx.body };
538
+ if (ctx.body && typeof ctx.body === "object") {
539
+ const data = ctx.body;
540
+ const fromKey = data[messageKey];
541
+ if (typeof fromKey === "string" && fromKey) return { message: fromKey };
542
+ if (typeof data.error === "string" && data.error) return { message: data.error };
543
+ }
544
+ if (ctx.raw instanceof Error && ctx.raw.message) return { message: ctx.raw.message };
545
+ return null;
546
+ };
547
+ //#endregion
548
+ //#region src/components/GrResponseErrorBanner/parsers/problemDetailsParser.ts
549
+ /**
550
+ * Парсер RFC 7807 «Problem Details for HTTP APIs»:
551
+ *
552
+ * ```json
553
+ * { "type": "https://example.com/probs/out-of-credit",
554
+ * "title": "You do not have enough credit.",
555
+ * "status": 403,
556
+ * "detail": "Your current balance is 30.",
557
+ * "errors": { "balance": ["..."] } }
558
+ * ```
559
+ *
560
+ * Активируется, если есть `body.type` + `body.title`, либо
561
+ * Content-Type заголовка `application/problem+json`. Берёт `title` как
562
+ * `message`, `detail` — в `details[0]`, status — в `status`.
563
+ */
564
+ var problemDetailsParser = (ctx) => {
565
+ const body = ctx.body;
566
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
567
+ const data = body;
568
+ const isProblemContentType = ctx.headers?.["content-type"]?.includes("application/problem+json");
569
+ const hasShape = typeof data.title === "string" && (typeof data.type === "string" || typeof data.status === "number");
570
+ if (!isProblemContentType && !hasShape) return null;
571
+ const message = typeof data.title === "string" ? data.title : ctx.texts.unknownMessage;
572
+ const details = [];
573
+ if (typeof data.detail === "string" && data.detail) details.push(data.detail);
574
+ const status = typeof data.status === "number" ? data.status : ctx.status;
575
+ return {
576
+ message,
577
+ details: details.length ? details : void 0,
578
+ status
579
+ };
580
+ };
581
+ //#endregion
582
+ //#region src/components/GrResponseErrorBanner/parsers/createResponseErrorClassifier.ts
583
+ /**
584
+ * Дефолтная цепочка парсеров. Порядок важен:
585
+ * `abort → network → fileValidation → laravel → problemDetails → jsonApi → httpStatus → plainMessage`.
586
+ *
587
+ * - Ранние специализированные парсеры могут поставить `stop` (abort/file).
588
+ * - HTTP-парсер даёт fallback-kind/message, чтобы остальное могло уточнить.
589
+ * - `plainMessage` всегда последний — добывает хоть какое-то сообщение.
590
+ */
591
+ var defaultResponseErrorParsers = [
592
+ abortErrorParser,
593
+ networkErrorParser,
594
+ fileValidationParser,
595
+ laravelValidationParser,
596
+ problemDetailsParser,
597
+ jsonApiErrorParser,
598
+ httpStatusParser,
599
+ plainMessageParser
600
+ ];
601
+ /** Добавляет пользовательские парсеры в начало дефолтной цепочки. */
602
+ function extendDefaultParsers(extra) {
603
+ return [...extra, ...defaultResponseErrorParsers];
604
+ }
605
+ /**
606
+ * Фабрика классификатора. Возвращает функцию `(raw, meta?) => Promise<ResponseErrorInfo>`,
607
+ * которая:
608
+ * 1. нормализует вход через `normalizeError`,
609
+ * 2. строит `ResponseErrorContext`,
610
+ * 3. прогоняет цепочку парсеров (с поддержкой `stop`),
611
+ * 4. собирает финальный `ResponseErrorInfo` с фолбэками из `texts`.
612
+ */
613
+ function createResponseErrorClassifier(options = {}) {
614
+ const parsers = options.parsers ?? defaultResponseErrorParsers;
615
+ const texts = {
616
+ ...DEFAULT_RESPONSE_ERROR_TEXTS,
617
+ ...options.texts ?? {}
618
+ };
619
+ const messageKey = options.messageKey ?? "message";
620
+ return async function classify(raw, meta = {}) {
621
+ const ctx = {
622
+ ...await normalizeError(raw),
623
+ meta: {
624
+ ...meta,
625
+ _messageKey: messageKey
626
+ },
627
+ texts
628
+ };
629
+ let kind;
630
+ let status = ctx.status;
631
+ let message;
632
+ let details;
633
+ let fieldErrors;
634
+ for (const parser of parsers) {
635
+ const result = parser(ctx);
636
+ if (!result) continue;
637
+ if (result.kind) kind = result.kind;
638
+ if (typeof result.status === "number") status = result.status;
639
+ if (result.message) message = result.message;
640
+ if (result.details?.length) details = result.details;
641
+ if (result.fieldErrors?.length) fieldErrors = result.fieldErrors;
642
+ if (result.stop) break;
643
+ }
644
+ if (!kind) kind = "unknown";
645
+ if (!message) message = resolveDefaultMessage(kind, texts);
646
+ return {
647
+ kind,
648
+ status,
649
+ message,
650
+ details,
651
+ fieldErrors,
652
+ raw,
653
+ meta
654
+ };
655
+ };
656
+ }
657
+ function resolveDefaultMessage(kind, texts) {
658
+ switch (kind) {
659
+ case "network": return texts.networkMessage;
660
+ case "aborted": return texts.abortedMessage;
661
+ case "validation": return texts.validationMessage;
662
+ case "client": return texts.clientMessage;
663
+ case "server": return texts.serverMessage;
664
+ default: return texts.unknownMessage;
665
+ }
666
+ }
667
+ function resolveResponseErrorTitle(kind, texts) {
668
+ switch (kind) {
669
+ case "network": return texts.networkTitle;
670
+ case "aborted": return texts.abortedTitle;
671
+ case "validation": return texts.validationTitle;
672
+ case "client": return texts.clientTitle;
673
+ case "server": return texts.serverTitle;
674
+ default: return texts.unknownTitle;
675
+ }
676
+ }
677
+ //#endregion
678
+ //#region src/components/GrResponseErrorBanner/GrResponseErrorBanner.vue?vue&type=script&setup=true&lang.ts
679
+ var _hoisted_1 = { class: "flex flex-col gap-2" };
680
+ var _hoisted_2 = { class: "flex flex-wrap items-center gap-2" };
681
+ var _hoisted_3 = ["data-testid"];
682
+ var _hoisted_4 = ["data-testid"];
683
+ var _hoisted_5 = ["data-testid"];
684
+ var _hoisted_6 = {
685
+ key: 1,
686
+ class: "mt-1 flex flex-wrap items-center gap-2"
687
+ };
688
+ //#endregion
689
+ //#region src/components/GrResponseErrorBanner/GrResponseErrorBanner.vue
690
+ var GrResponseErrorBanner_default = /* @__PURE__ */ defineComponent({
691
+ __name: "GrResponseErrorBanner",
692
+ props: {
693
+ error: {},
694
+ texts: { default: () => ({}) },
695
+ tone: { default: void 0 },
696
+ toneByKind: { default: () => ({}) },
697
+ showDetails: {
698
+ type: Boolean,
699
+ default: true
700
+ },
701
+ showFieldLabels: {
702
+ type: Boolean,
703
+ default: true
704
+ },
705
+ fieldLabels: { default: () => ({}) },
706
+ dedupeDetails: {
707
+ type: Boolean,
708
+ default: true
709
+ },
710
+ canRetry: {
711
+ type: Boolean,
712
+ default: false
713
+ },
714
+ canDismiss: {
715
+ type: Boolean,
716
+ default: true
717
+ },
718
+ autoHideKinds: { default: () => DEFAULT_AUTO_HIDE_KINDS },
719
+ showStatus: {
720
+ type: Boolean,
721
+ default: true
722
+ },
723
+ testIdPrefix: { default: "response-error" }
724
+ },
725
+ emits: ["retry", "dismiss"],
726
+ setup(__props, { emit: __emit }) {
727
+ const props = __props;
728
+ const emit = __emit;
729
+ const { t } = useGranularityTranslations();
730
+ const i18nTexts = computed(() => {
731
+ const out = {};
732
+ for (const key of Object.keys(DEFAULT_RESPONSE_ERROR_TEXTS)) out[key] = t(`gr.responseError.${key}`, DEFAULT_RESPONSE_ERROR_TEXTS[key]);
733
+ return out;
734
+ });
735
+ const mergedTexts = computed(() => ({
736
+ ...DEFAULT_RESPONSE_ERROR_TEXTS,
737
+ ...i18nTexts.value,
738
+ ...props.texts
739
+ }));
740
+ const mergedToneByKind = computed(() => ({
741
+ ...DEFAULT_TONE_BY_KIND,
742
+ ...props.toneByKind
743
+ }));
744
+ const isHidden = computed(() => {
745
+ if (!props.error) return true;
746
+ return props.autoHideKinds.includes(props.error.kind);
747
+ });
748
+ const tone = computed(() => {
749
+ if (props.tone) return props.tone;
750
+ if (!props.error) return "danger";
751
+ return mergedToneByKind.value[props.error.kind] ?? "danger";
752
+ });
753
+ const title = computed(() => {
754
+ if (!props.error) return "";
755
+ return resolveResponseErrorTitle(props.error.kind, mergedTexts.value);
756
+ });
757
+ /**
758
+ * Маппинг `kind` → ключ поля сообщения в `ResponseErrorTexts`.
759
+ * Используется, чтобы подменить «дефолтное английское» fallback-сообщение
760
+ * классификатора на i18n-перевод (или текущий `mergedTexts.<kind>Message`).
761
+ */
762
+ const KIND_MESSAGE_KEY = {
763
+ network: "networkMessage",
764
+ aborted: "abortedMessage",
765
+ validation: "validationMessage",
766
+ client: "clientMessage",
767
+ server: "serverMessage",
768
+ unknown: "unknownMessage"
769
+ };
770
+ const DEFAULT_FALLBACK_MESSAGES = new Set([
771
+ DEFAULT_RESPONSE_ERROR_TEXTS.networkMessage,
772
+ DEFAULT_RESPONSE_ERROR_TEXTS.abortedMessage,
773
+ DEFAULT_RESPONSE_ERROR_TEXTS.validationMessage,
774
+ DEFAULT_RESPONSE_ERROR_TEXTS.clientMessage,
775
+ DEFAULT_RESPONSE_ERROR_TEXTS.serverMessage,
776
+ DEFAULT_RESPONSE_ERROR_TEXTS.unknownMessage
777
+ ]);
778
+ const message = computed(() => {
779
+ const raw = props.error?.message ?? "";
780
+ if (!props.error) return raw;
781
+ if (!raw || DEFAULT_FALLBACK_MESSAGES.has(raw)) return mergedTexts.value[KIND_MESSAGE_KEY[props.error.kind]];
782
+ return raw;
783
+ });
784
+ /**
785
+ * Сводный список «деталей» под основным `message`.
786
+ * Предпочитаем `fieldErrors` (поле → сообщения), затем — плоский `details`.
787
+ * Дедуплицируем и убираем повтор с `message`.
788
+ */
789
+ const detailEntries = computed(() => {
790
+ if (!props.error || !props.showDetails) return [];
791
+ const seen = /* @__PURE__ */ new Set();
792
+ if (props.dedupeDetails) {
793
+ const main = message.value.trim();
794
+ if (main) seen.add(main);
795
+ }
796
+ const result = [];
797
+ const fieldErrors = props.error.fieldErrors;
798
+ if (fieldErrors?.length) {
799
+ for (const fe of fieldErrors) for (const m of fe.messages) {
800
+ const text = m.trim();
801
+ if (!text || seen.has(text)) continue;
802
+ seen.add(text);
803
+ result.push({
804
+ field: fe.field,
805
+ text
806
+ });
807
+ }
808
+ if (result.length) return result;
809
+ }
810
+ const details = props.error.details;
811
+ if (details?.length) for (const d of details) {
812
+ const text = (d ?? "").trim();
813
+ if (!text || seen.has(text)) continue;
814
+ seen.add(text);
815
+ result.push({ text });
816
+ }
817
+ return result;
818
+ });
819
+ function fieldLabel(field) {
820
+ if (!field) return void 0;
821
+ return props.fieldLabels[field] ?? field;
822
+ }
823
+ function onRetry() {
824
+ if (props.error) emit("retry", props.error);
825
+ }
826
+ function onDismiss() {
827
+ emit("dismiss");
828
+ }
829
+ return (_ctx, _cache) => {
830
+ return !isHidden.value && props.error ? (openBlock(), createBlock(GrAlert_default, {
831
+ key: 0,
832
+ tone: tone.value,
833
+ title: title.value,
834
+ closable: props.canDismiss,
835
+ "data-testid": `${props.testIdPrefix}-banner`,
836
+ "data-kind": props.error.kind,
837
+ onClose: onDismiss
838
+ }, {
839
+ default: withCtx(() => [createElementVNode("div", _hoisted_1, [
840
+ createElementVNode("div", _hoisted_2, [props.showStatus && typeof props.error.status === "number" ? (openBlock(), createElementBlock("span", {
841
+ key: 0,
842
+ class: "inline-flex items-center rounded-[var(--ds-radius-sm)] bg-[color-mix(in_srgb,currentColor_12%,transparent)] px-1.5 py-0.5 text-[11px] font-700 leading-none",
843
+ "data-testid": `${props.testIdPrefix}-status`
844
+ }, " HTTP " + toDisplayString(props.error.status), 9, _hoisted_3)) : createCommentVNode("", true), createElementVNode("span", {
845
+ class: "text-[13px]",
846
+ "data-testid": `${props.testIdPrefix}-message`
847
+ }, toDisplayString(message.value), 9, _hoisted_4)]),
848
+ detailEntries.value.length ? (openBlock(), createElementBlock("ul", {
849
+ key: 0,
850
+ class: "m-0 list-disc pl-5 text-[12px]",
851
+ "data-testid": `${props.testIdPrefix}-details`
852
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(detailEntries.value, (entry, idx) => {
853
+ return openBlock(), createElementBlock("li", { key: `${entry.field ?? ""}:${idx}` }, [props.showFieldLabels && entry.field ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("strong", null, toDisplayString(fieldLabel(entry.field)) + ":", 1), _cache[0] || (_cache[0] = createTextVNode("\xA0 ", -1))], 64)) : createCommentVNode("", true), createTextVNode(toDisplayString(entry.text), 1)]);
854
+ }), 128))], 8, _hoisted_5)) : createCommentVNode("", true),
855
+ props.canRetry ? (openBlock(), createElementBlock("div", _hoisted_6, [createVNode(GrButton_default, {
856
+ size: "sm",
857
+ variant: "outline",
858
+ "data-testid": `${props.testIdPrefix}-retry`,
859
+ onClick: onRetry
860
+ }, {
861
+ default: withCtx(() => [createTextVNode(toDisplayString(mergedTexts.value.retryLabel), 1)]),
862
+ _: 1
863
+ }, 8, ["data-testid"])])) : createCommentVNode("", true)
864
+ ])]),
865
+ _: 1
866
+ }, 8, [
867
+ "tone",
868
+ "title",
869
+ "closable",
870
+ "data-testid",
871
+ "data-kind"
872
+ ])) : createCommentVNode("", true);
873
+ };
874
+ }
875
+ });
876
+ //#endregion
877
+ //#region src/components/GrResponseErrorBanner/GrUploadErrorBanner.vue
878
+ var GrUploadErrorBanner_default = /* @__PURE__ */ defineComponent({
879
+ __name: "GrUploadErrorBanner",
880
+ props: {
881
+ error: {},
882
+ files: { default: () => [] },
883
+ texts: { default: () => ({}) },
884
+ tone: { default: void 0 },
885
+ toneByKind: { default: () => ({}) },
886
+ canRetry: {
887
+ type: Boolean,
888
+ default: true
889
+ },
890
+ canDismiss: {
891
+ type: Boolean,
892
+ default: true
893
+ },
894
+ autoHideKinds: { default: () => [] },
895
+ testIdPrefix: { default: "upload-error" }
896
+ },
897
+ emits: ["retry", "dismiss"],
898
+ setup(__props, { emit: __emit }) {
899
+ /**
900
+ * Тонкая обёртка над `GrResponseErrorBanner` с предсетами для сценария
901
+ * «загрузка файлов»:
902
+ *
903
+ * - Включена кнопка «Повторить» по умолчанию (`canRetry=true`).
904
+ * - Тексты сдвинуты под загрузку файлов.
905
+ * - Опциональный prop `files` пробрасывает контекст наружу через `retry`.
906
+ *
907
+ * Парсеры/нормализация — те же, что у базового баннера. Если нужны кастомные
908
+ * парсеры — используйте напрямую `GrResponseErrorBanner` + `useResponseError`.
909
+ */
910
+ const props = __props;
911
+ const emit = __emit;
912
+ const UPLOAD_TEXTS = {
913
+ networkTitle: "Failed to upload files",
914
+ networkMessage: "Check your connection and try again.",
915
+ validationTitle: "Files did not pass validation",
916
+ validationMessage: "Some of the files do not meet the requirements.",
917
+ serverTitle: "Upload error on the server",
918
+ serverMessage: "The server could not process the upload. Please try again."
919
+ };
920
+ const mergedTexts = computed(() => ({
921
+ ...UPLOAD_TEXTS,
922
+ ...props.texts
923
+ }));
924
+ function onRetry(info) {
925
+ emit("retry", {
926
+ error: info,
927
+ files: props.files
928
+ });
929
+ }
930
+ return (_ctx, _cache) => {
931
+ return openBlock(), createBlock(GrResponseErrorBanner_default, {
932
+ error: props.error,
933
+ texts: mergedTexts.value,
934
+ tone: props.tone,
935
+ "tone-by-kind": props.toneByKind,
936
+ "can-retry": props.canRetry,
937
+ "can-dismiss": props.canDismiss,
938
+ "auto-hide-kinds": props.autoHideKinds,
939
+ "test-id-prefix": props.testIdPrefix,
940
+ onRetry,
941
+ onDismiss: _cache[0] || (_cache[0] = ($event) => emit("dismiss"))
942
+ }, null, 8, [
943
+ "error",
944
+ "texts",
945
+ "tone",
946
+ "tone-by-kind",
947
+ "can-retry",
948
+ "can-dismiss",
949
+ "auto-hide-kinds",
950
+ "test-id-prefix"
951
+ ]);
952
+ };
953
+ }
954
+ });
955
+ //#endregion
956
+ //#region src/components/GrResponseErrorBanner/GrFormErrorBanner.vue
957
+ var GrFormErrorBanner_default = /* @__PURE__ */ defineComponent({
958
+ __name: "GrFormErrorBanner",
959
+ props: {
960
+ error: {},
961
+ fieldLabels: { default: () => ({}) },
962
+ texts: { default: () => ({}) },
963
+ tone: { default: void 0 },
964
+ toneByKind: { default: () => ({ validation: "warning" }) },
965
+ canRetry: {
966
+ type: Boolean,
967
+ default: false
968
+ },
969
+ canDismiss: {
970
+ type: Boolean,
971
+ default: true
972
+ },
973
+ autoHideKinds: { default: () => [] },
974
+ testIdPrefix: { default: "form-error" }
975
+ },
976
+ emits: ["retry", "dismiss"],
977
+ setup(__props, { emit: __emit }) {
978
+ /**
979
+ * Тонкая обёртка над `GrResponseErrorBanner` с предсетами для сценария
980
+ * «валидация формы»:
981
+ *
982
+ * - `showFieldLabels=true` — рисуются подписи `field:` перед каждой строкой.
983
+ * - `canRetry=false` — обычно пользователь правит поля сам, retry не нужен.
984
+ * - `toneByKind.validation = 'warning'` — визуально «исправь», не «упало».
985
+ * - Тексты сдвинуты под формы.
986
+ *
987
+ * Для кастомных парсеров используйте напрямую `GrResponseErrorBanner` +
988
+ * `useResponseError({ parsers: () => extendDefaultParsers([myParser]) })`.
989
+ */
990
+ const props = __props;
991
+ const emit = __emit;
992
+ const FORM_TEXTS = {
993
+ validationTitle: "Check the form fields",
994
+ validationMessage: "The server rejected the data. Fix the highlighted fields and try again.",
995
+ clientTitle: "Failed to save",
996
+ serverTitle: "Save error",
997
+ serverMessage: "Could not save the data. Please try again later."
998
+ };
999
+ const mergedTexts = computed(() => ({
1000
+ ...FORM_TEXTS,
1001
+ ...props.texts
1002
+ }));
1003
+ return (_ctx, _cache) => {
1004
+ return openBlock(), createBlock(GrResponseErrorBanner_default, {
1005
+ error: props.error,
1006
+ texts: mergedTexts.value,
1007
+ tone: props.tone,
1008
+ "tone-by-kind": props.toneByKind,
1009
+ "field-labels": props.fieldLabels,
1010
+ "show-field-labels": "",
1011
+ "can-retry": props.canRetry,
1012
+ "can-dismiss": props.canDismiss,
1013
+ "auto-hide-kinds": props.autoHideKinds,
1014
+ "test-id-prefix": props.testIdPrefix,
1015
+ onRetry: _cache[0] || (_cache[0] = (info) => emit("retry", info)),
1016
+ onDismiss: _cache[1] || (_cache[1] = ($event) => emit("dismiss"))
1017
+ }, null, 8, [
1018
+ "error",
1019
+ "texts",
1020
+ "tone",
1021
+ "tone-by-kind",
1022
+ "field-labels",
1023
+ "can-retry",
1024
+ "can-dismiss",
1025
+ "auto-hide-kinds",
1026
+ "test-id-prefix"
1027
+ ]);
1028
+ };
1029
+ }
1030
+ });
1031
+ //#endregion
1032
+ //#region src/components/GrResponseErrorBanner/useResponseError.ts
1033
+ /**
1034
+ * Stateful-композабл вокруг `createResponseErrorClassifier`. Хранит
1035
+ * `currentError`, инкапсулирует setRaw/retry/dismiss. Подходит для типичного
1036
+ * сценария «поймал ошибку запроса → показал баннер → ретрай/скрыть».
1037
+ */
1038
+ function useResponseError(options = {}) {
1039
+ const currentError = shallowRef(null);
1040
+ const lastRaw = ref(null);
1041
+ function getClassifier() {
1042
+ return createResponseErrorClassifier({
1043
+ parsers: options.parsers?.() ?? defaultResponseErrorParsers,
1044
+ texts: options.texts?.() ?? {},
1045
+ messageKey: options.messageKey?.()
1046
+ });
1047
+ }
1048
+ function isAutoHidden(kind) {
1049
+ return (options.autoHideKinds?.() ?? DEFAULT_AUTO_HIDE_KINDS).includes(kind);
1050
+ }
1051
+ async function classify(raw, extra) {
1052
+ const info = await getClassifier()(raw, {
1053
+ ...options.meta?.() ?? {},
1054
+ ...extra ?? {}
1055
+ });
1056
+ options.onClassify?.(info);
1057
+ return info;
1058
+ }
1059
+ async function setRaw(raw, extra) {
1060
+ lastRaw.value = raw;
1061
+ const info = await classify(raw, extra);
1062
+ if (isAutoHidden(info.kind)) {
1063
+ currentError.value = null;
1064
+ return null;
1065
+ }
1066
+ currentError.value = info;
1067
+ return info;
1068
+ }
1069
+ function setError(info) {
1070
+ currentError.value = info;
1071
+ }
1072
+ function dismiss() {
1073
+ currentError.value = null;
1074
+ }
1075
+ async function retry(handler) {
1076
+ const info = currentError.value;
1077
+ if (!info) return;
1078
+ await handler(info);
1079
+ currentError.value = null;
1080
+ }
1081
+ return {
1082
+ currentError,
1083
+ isVisible: computed(() => currentError.value !== null),
1084
+ lastRaw,
1085
+ setRaw,
1086
+ setError,
1087
+ classify,
1088
+ dismiss,
1089
+ reset: dismiss,
1090
+ retry
1091
+ };
1092
+ }
1093
+ //#endregion
121
1094
  //#region src/components/shared/tones.ts
122
1095
  /**
123
1096
  * Единый перечень дизайн-тонов для всех компонентов библиотеки.
@@ -138,4 +1111,4 @@ var GR_TONES = [
138
1111
  "azure"
139
1112
  ];
140
1113
  //#endregion
141
- export { DEFAULT_GR_DIALOG_BODY_CONFIG, DEFAULT_GR_DIALOG_FOOTER_CONFIG, DEFAULT_GR_DIALOG_HEADER_CONFIG, FileValidationError, GR_COLLAPSE_CONTEXT, GR_RADIO_GROUP_CONTEXT, GR_TONES, GR_UPLOAD_STATE_IDLE, GrAlert_default as GrAlert, GrAvatar_default as GrAvatar, GrBadge_default as GrBadge, GrBadgeWrap_default as GrBadgeWrap, GrBottomNav_default as GrBottomNav, GrButton_default as GrButton, GrButtonGroup_default as GrButtonGroup, GrCard_default as GrCard, GrCheckbox_default as GrCheckbox, GrCollapse_default as GrCollapse, GrCollapseItem_default as GrCollapseItem, GrConfirmDialog_default as GrConfirmDialog, GrDataTable_default as GrDataTable, GrDialog_default as GrDialog, GrDialogCloseButton_default as GrDialogCloseButton, GrDialogFooter_default as GrDialogFooter, GrDialogHeader_default as GrDialogHeader, GrDrawer_default as GrDrawer, GrDropdown_default as GrDropdown, GrDropdownMenu_default as GrDropdownMenu, GrDropdownMenuColumn_default as GrDropdownMenuColumn, GrDropdownMenuColumns_default as GrDropdownMenuColumns, GrDropdownMenuDivider_default as GrDropdownMenuDivider, GrDropdownMenuGroup_default as GrDropdownMenuGroup, GrDropdownMenuHeader_default as GrDropdownMenuHeader, GrDropdownMenuItem_default as GrDropdownMenuItem, GrDropdownMenuList_default as GrDropdownMenuList, GrEmptyState_default as GrEmptyState, GrFileUpload_default as GrFileUpload, GrFormField_default as GrFormField, GrFormFile_default as GrFormFile, GrFormSection_default as GrFormSection, GrIcon_default as GrIcon, GrImageViewer_default as GrImageViewer, GrInput_default as GrInput, GrInputTag_default as GrInputTag, GrLink_default as GrLink, GrList_default as GrList, GrListItem_default as GrListItem, GrLoading_default as GrLoading, GrModal_default as GrModal, GrNavbar_default as GrNavbar, GrNumberInput_default as GrNumberInput, GrPagination_default as GrPagination, GrProgressBar_default as GrProgressBar, GrPromptDialog_default as GrPromptDialog, GrRadio_default as GrRadio, GrRadioGroup_default as GrRadioGroup, GrSegmented_default as GrSegmented, GrSelect_default as GrSelect, GrSidebar_default as GrSidebar, GrSkeleton_default as GrSkeleton, GrSwitch_default as GrSwitch, GrTable_default as GrTable, GrTabs_default as GrTabs, GrTextarea_default as GrTextarea, GrToaster_default as GrToaster, GrTooltip_default as GrTooltip, GrTree_default as GrTree, GrTreeSelect_default as GrTreeSelect, GrUploadAbortError, GrUploadHttpError, acceptValidator, allowedExtensionsValidator, allowedMimeTypesValidator, createLoading, getGrModalPanelClass, grAlertConfig, grAvatarConfig, grAvatarSafelist, grBadgeConfig, grBadgeSafelist, grBadgeWrapConfig, grBottomNavConfig, grButtonClass, grButtonClassTokens, grButtonConfig, grButtonGroupConfig, grButtonSafelist, grCardConfig, grCheckboxConfig, grCollapseConfig, grConfirmDialogConfig, grDataTableConfig, grDialogSafelist, grDrawerConfig, grDrawerSafelist, grDropdownConfig, grDropdownMenuConfig, grDropdownSafelist, grEmptyStateConfig, grFileUploadConfig, grFormFieldConfig, grFormFileConfig, grFormSectionConfig, grIconConfig, grImageViewerConfig, grInputConfig, grInputSafelist, grInputTagConfig, grInputTagSafelist, grLinkConfig, grLinkSafelist, grListConfig, grLoadingConfig, grLoadingSafelist, grModalClassTokens, grModalConfig, grModalSafelist, grNavbarConfig, grNumberInputConfig, grNumberInputSafelist, grPaginationConfig, grProgressBarConfig, grProgressBarSafelist, grPromptDialogConfig, grRadioConfig, grRadioGroupConfig, grRadioSafelist, grSegmentedConfig, grSegmentedSafelist, grSelectConfig, grSelectSafelist, grSidebarConfig, grSkeletonConfig, grSwitchConfig, grSwitchSafelist, grTableConfig, grTabsConfig, grTextareaConfig, grTextareaSafelist, grToasterConfig, grToasterSafelist, grTooltipConfig, grTreeConfig, grTreeSafelist, grTreeSelectConfig, grTreeSelectSafelist, initThemeEarly, matchAccept, maxFileSizeBytesValidator, maxSizeMbValidator, maxTotalSizeBytesValidator, normalizeFiles, resolveGrDialogSectionConfig, resolveGrDialogTitle, runFileValidators, useTheme, useToast, vAutofocus, vAutosize, vClickOutside, vDropzone, vHotkey, vLoading };
1114
+ export { DEFAULT_AUTO_HIDE_KINDS, DEFAULT_GR_DIALOG_BODY_CONFIG, DEFAULT_GR_DIALOG_FOOTER_CONFIG, DEFAULT_GR_DIALOG_HEADER_CONFIG, DEFAULT_RESPONSE_ERROR_TEXTS, DEFAULT_TONE_BY_KIND, FileValidationError, GR_COLLAPSE_CONTEXT, GR_RADIO_GROUP_CONTEXT, GR_TONES, GR_UPLOAD_STATE_IDLE, GrAlert_default as GrAlert, GrAvatar_default as GrAvatar, GrBadge_default as GrBadge, GrBadgeWrap_default as GrBadgeWrap, GrBottomNav_default as GrBottomNav, GrButton_default as GrButton, GrButtonGroup_default as GrButtonGroup, GrCard_default as GrCard, GrCheckbox_default as GrCheckbox, GrCollapse_default as GrCollapse, GrCollapseItem_default as GrCollapseItem, GrConfirmDialog_default as GrConfirmDialog, GrDataTable_default as GrDataTable, GrDialog_default as GrDialog, GrDialogCloseButton_default as GrDialogCloseButton, GrDialogFooter_default as GrDialogFooter, GrDialogHeader_default as GrDialogHeader, GrDrawer_default as GrDrawer, GrDropdown_default as GrDropdown, GrDropdownMenu_default as GrDropdownMenu, GrDropdownMenuColumn_default as GrDropdownMenuColumn, GrDropdownMenuColumns_default as GrDropdownMenuColumns, GrDropdownMenuDivider_default as GrDropdownMenuDivider, GrDropdownMenuGroup_default as GrDropdownMenuGroup, GrDropdownMenuHeader_default as GrDropdownMenuHeader, GrDropdownMenuItem_default as GrDropdownMenuItem, GrDropdownMenuList_default as GrDropdownMenuList, GrEmptyState_default as GrEmptyState, GrFileUpload_default as GrFileUpload, GrFormErrorBanner_default as GrFormErrorBanner, GrFormField_default as GrFormField, GrFormFile_default as GrFormFile, GrFormSection_default as GrFormSection, GrIcon_default as GrIcon, GrImageViewer_default as GrImageViewer, GrInput_default as GrInput, GrInputTag_default as GrInputTag, GrLink_default as GrLink, GrList_default as GrList, GrListItem_default as GrListItem, GrLoading_default as GrLoading, GrModal_default as GrModal, GrNavbar_default as GrNavbar, GrNumberInput_default as GrNumberInput, GrPagination_default as GrPagination, GrProgressBar_default as GrProgressBar, GrPromptDialog_default as GrPromptDialog, GrRadio_default as GrRadio, GrRadioGroup_default as GrRadioGroup, GrResponseErrorBanner_default as GrResponseErrorBanner, GrSegmented_default as GrSegmented, GrSelect_default as GrSelect, GrSidebar_default as GrSidebar, GrSkeleton_default as GrSkeleton, GrSwitch_default as GrSwitch, GrTable_default as GrTable, GrTabs_default as GrTabs, GrTextarea_default as GrTextarea, GrToaster_default as GrToaster, GrTooltip_default as GrTooltip, GrTree_default as GrTree, GrTreeSelect_default as GrTreeSelect, GrUploadAbortError, GrUploadErrorBanner_default as GrUploadErrorBanner, GrUploadHttpError, abortErrorParser, acceptValidator, allowedExtensionsValidator, allowedMimeTypesValidator, createLoading, createResponseErrorClassifier, defaultResponseErrorParsers, extendDefaultParsers, fileValidationParser, getGrModalPanelClass, grAlertConfig, grAvatarConfig, grAvatarSafelist, grBadgeConfig, grBadgeSafelist, grBadgeWrapConfig, grBottomNavConfig, grButtonClass, grButtonClassTokens, grButtonConfig, grButtonGroupConfig, grButtonSafelist, grCardConfig, grCheckboxConfig, grCollapseConfig, grConfirmDialogConfig, grDataTableConfig, grDialogSafelist, grDrawerConfig, grDrawerSafelist, grDropdownConfig, grDropdownMenuConfig, grDropdownSafelist, grEmptyStateConfig, grFileUploadConfig, grFormFieldConfig, grFormFileConfig, grFormSectionConfig, grIconConfig, grImageViewerConfig, grInputConfig, grInputSafelist, grInputTagConfig, grInputTagSafelist, grLinkConfig, grLinkSafelist, grListConfig, grLoadingConfig, grLoadingSafelist, grModalClassTokens, grModalConfig, grModalSafelist, grNavbarConfig, grNumberInputConfig, grNumberInputSafelist, grPaginationConfig, grProgressBarConfig, grProgressBarSafelist, grPromptDialogConfig, grRadioConfig, grRadioGroupConfig, grRadioSafelist, grSegmentedConfig, grSegmentedSafelist, grSelectConfig, grSelectSafelist, grSidebarConfig, grSkeletonConfig, grSwitchConfig, grSwitchSafelist, grTableConfig, grTabsConfig, grTextareaConfig, grTextareaSafelist, grToasterConfig, grToasterSafelist, grTooltipConfig, grTreeConfig, grTreeSafelist, grTreeSelectConfig, grTreeSelectSafelist, httpStatusParser, initThemeEarly, jsonApiErrorParser, laravelValidationParser, matchAccept, maxFileSizeBytesValidator, maxSizeMbValidator, maxTotalSizeBytesValidator, networkErrorParser, normalizeError, normalizeFiles, plainMessageParser, problemDetailsParser, resolveGrDialogSectionConfig, resolveGrDialogTitle, resolveResponseErrorTitle, runFileValidators, useResponseError, useTheme, useToast, vAutofocus, vAutosize, vClickOutside, vDropzone, vHotkey, vLoading };