@feugene/granularity 0.41.0 → 0.42.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 (83) hide show
  1. package/LICENSE +13 -162
  2. package/README.md +4 -4
  3. package/dist/chunks/{config-BRESvs9g.js → config-Ca8qr8Q3.js} +1 -1
  4. package/dist/chunks/{config-BRESvs9g.js.map → config-Ca8qr8Q3.js.map} +1 -1
  5. package/dist/chunks/{en-CuuLfNHX.js → en-B9E3jO6m.js} +5 -1
  6. package/dist/chunks/{en-CuuLfNHX.js.map → en-B9E3jO6m.js.map} +1 -1
  7. package/dist/chunks/{es-CsmvfESn.js → es-DCkqHTlU.js} +3 -3
  8. package/dist/chunks/{es-CsmvfESn.js.map → es-DCkqHTlU.js.map} +1 -1
  9. package/dist/chunks/{es-CSnmrcMw.js → es-QpzdIEut.js} +5 -1
  10. package/dist/chunks/{es-CSnmrcMw.js.map → es-QpzdIEut.js.map} +1 -1
  11. package/dist/chunks/{grKbdStyles-DENu8pUK.js → grKbdStyles-ByMsX-lQ.js} +17 -2
  12. package/dist/chunks/grKbdStyles-ByMsX-lQ.js.map +1 -0
  13. package/dist/chunks/{granular-provider-BshNi2la.js → granular-provider-BG_2XqkS.js} +1 -1
  14. package/dist/chunks/{granular-provider-BshNi2la.js.map → granular-provider-BG_2XqkS.js.map} +1 -1
  15. package/dist/chunks/{ru-BKWafECI.js → ru-D_1Ya489.js} +5 -1
  16. package/dist/chunks/{ru-BKWafECI.js.map → ru-D_1Ya489.js.map} +1 -1
  17. package/dist/components/GrBreadcrumbs/chunks/{GrBreadcrumbs-DOpwY2EF.js → GrBreadcrumbs-D0qPry_R.js} +4 -1
  18. package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-D0qPry_R.js.map +1 -0
  19. package/dist/components/GrBreadcrumbs/index.js +1 -1
  20. package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-CnzPuoIM.js → GrCommandPalette-Ha4s3DMT.js} +1 -1
  21. package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-CnzPuoIM.js.map → GrCommandPalette-Ha4s3DMT.js.map} +1 -1
  22. package/dist/components/GrCommandPalette/index.js +1 -1
  23. package/dist/components/GrDataTable/chunks/{GrDataTable-DpyipOFx.js → GrDataTable-BBe7-uUe.js} +5 -1
  24. package/dist/components/GrDataTable/chunks/GrDataTable-BBe7-uUe.js.map +1 -0
  25. package/dist/components/GrDataTable/index.js +1 -1
  26. package/dist/components/GrDelta/chunks/{GrDelta-BpfqebEV.js → GrDelta-CgTvfoIN.js} +0 -0
  27. package/dist/components/GrDelta/chunks/GrDelta-CgTvfoIN.js.map +1 -0
  28. package/dist/components/GrDelta/index.js +1 -1
  29. package/dist/components/GrDescriptionList/chunks/{GrDescriptionList-D8ANFpn_.js → GrDescriptionList-Dz2Jj1if.js} +26 -11
  30. package/dist/components/GrDescriptionList/chunks/GrDescriptionList-Dz2Jj1if.js.map +1 -0
  31. package/dist/components/GrDescriptionList/index.js +1 -1
  32. package/dist/components/GrFilePreview/chunks/{GrFilePreview-PabkLgYX.js → GrFilePreview-2kVOg1C7.js} +40 -5
  33. package/dist/components/GrFilePreview/chunks/GrFilePreview-2kVOg1C7.js.map +1 -0
  34. package/dist/components/GrFilePreview/index.js +1 -1
  35. package/dist/components/GrInputTag/chunks/{GrInputTag-3Au5HWZL.js → GrInputTag-BKzm1PKw.js} +4 -1
  36. package/dist/components/GrInputTag/chunks/GrInputTag-BKzm1PKw.js.map +1 -0
  37. package/dist/components/GrInputTag/index.js +1 -1
  38. package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-CDGOX3C0.js → GrJsonViewer-BLCEWlwg.js} +32 -6
  39. package/dist/components/GrJsonViewer/chunks/GrJsonViewer-BLCEWlwg.js.map +1 -0
  40. package/dist/components/GrJsonViewer/index.js +1 -1
  41. package/dist/components/GrKbd/chunks/{GrKbd-D_a1mPLe.js → GrKbd-BNMeoaLV.js} +1 -1
  42. package/dist/components/GrKbd/chunks/{GrKbd-D_a1mPLe.js.map → GrKbd-BNMeoaLV.js.map} +1 -1
  43. package/dist/components/GrKbd/index.js +2 -2
  44. package/dist/components/GrNumberInput/chunks/{GrNumberInput-BqPydXpK.js → GrNumberInput-Bm-V59TG.js} +4 -1
  45. package/dist/components/GrNumberInput/chunks/GrNumberInput-Bm-V59TG.js.map +1 -0
  46. package/dist/components/GrNumberInput/index.js +1 -1
  47. package/dist/components/GrSegmented/chunks/{GrSegmented-NKTi9bMG.js → GrSegmented-CuDaKA6S.js} +5 -1
  48. package/dist/components/GrSegmented/chunks/GrSegmented-CuDaKA6S.js.map +1 -0
  49. package/dist/components/GrSegmented/index.js +1 -1
  50. package/dist/components/GrSortableList/chunks/{GrSortableList-CQCaWivn.js → GrSortableList-ce1psLL0.js} +4 -1
  51. package/dist/components/GrSortableList/chunks/GrSortableList-ce1psLL0.js.map +1 -0
  52. package/dist/components/GrSortableList/index.js +1 -1
  53. package/dist/components/GrSteps/chunks/{GrSteps-DXj7m-WO.js → GrSteps-Cng-Wrjk.js} +5 -1
  54. package/dist/components/GrSteps/chunks/GrSteps-Cng-Wrjk.js.map +1 -0
  55. package/dist/components/GrSteps/index.js +1 -1
  56. package/dist/components/GrTabs/chunks/{GrTabs-DHfv67cp.js → GrTabs-BArefUFv.js} +5 -1
  57. package/dist/components/GrTabs/chunks/GrTabs-BArefUFv.js.map +1 -0
  58. package/dist/components/GrTabs/index.js +1 -1
  59. package/dist/granular-provider-node.js +1 -1
  60. package/dist/granular-provider.js +1 -1
  61. package/dist/i18n/all.js +1 -1
  62. package/dist/i18n/index.js +1 -1
  63. package/dist/index.js +15 -15
  64. package/dist/types/components/GrFilePreview/GrFilePreview.vue.d.ts +14 -0
  65. package/dist/types/components/GrKbd/grKbdStyles.d.ts +17 -2
  66. package/dist/types/i18n/messages/en.d.ts +12 -0
  67. package/dist/types/i18n/messages/es.d.ts +12 -0
  68. package/dist/types/i18n/messages/ru.d.ts +12 -0
  69. package/dist/web-types.json +19 -1
  70. package/package.json +3 -3
  71. package/dist/chunks/grKbdStyles-DENu8pUK.js.map +0 -1
  72. package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-DOpwY2EF.js.map +0 -1
  73. package/dist/components/GrDataTable/chunks/GrDataTable-DpyipOFx.js.map +0 -1
  74. package/dist/components/GrDelta/chunks/GrDelta-BpfqebEV.js.map +0 -1
  75. package/dist/components/GrDescriptionList/chunks/GrDescriptionList-D8ANFpn_.js.map +0 -1
  76. package/dist/components/GrFilePreview/chunks/GrFilePreview-PabkLgYX.js.map +0 -1
  77. package/dist/components/GrInputTag/chunks/GrInputTag-3Au5HWZL.js.map +0 -1
  78. package/dist/components/GrJsonViewer/chunks/GrJsonViewer-CDGOX3C0.js.map +0 -1
  79. package/dist/components/GrNumberInput/chunks/GrNumberInput-BqPydXpK.js.map +0 -1
  80. package/dist/components/GrSegmented/chunks/GrSegmented-NKTi9bMG.js.map +0 -1
  81. package/dist/components/GrSortableList/chunks/GrSortableList-CQCaWivn.js.map +0 -1
  82. package/dist/components/GrSteps/chunks/GrSteps-DXj7m-WO.js.map +0 -1
  83. package/dist/components/GrTabs/chunks/GrTabs-DHfv67cp.js.map +0 -1
@@ -1,3 +1,3 @@
1
1
  import { n as grDescriptionListSafelist, t as grDescriptionListConfig } from "../../chunks/config-CnYdE3HZ.js";
2
- import { t as GrDescriptionList_default } from "./chunks/GrDescriptionList-D8ANFpn_.js";
2
+ import { t as GrDescriptionList_default } from "./chunks/GrDescriptionList-Dz2Jj1if.js";
3
3
  export { GrDescriptionList_default as GrDescriptionList, GrDescriptionList_default as default, grDescriptionListConfig, grDescriptionListSafelist };
@@ -1,7 +1,8 @@
1
1
  import { i as useGrComponentProp } from "../../../chunks/context-CPMRfwVr.js";
2
+ import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
2
3
  import { t as GrSkeleton_default } from "../../GrSkeleton/chunks/GrSkeleton-CgfXxPQ2.js";
3
4
  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-BaWANzzB.js";
4
- import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, defineComponent, markRaw, normalizeClass, normalizeStyle, openBlock, ref, resolveDynamicComponent, toDisplayString, unref, watch, withCtx } from "vue";
5
+ import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, defineComponent, markRaw, normalizeClass, normalizeStyle, openBlock, ref, resolveDynamicComponent, toDisplayString, unref, watch, watchEffect, withCtx } from "vue";
5
6
  //#region ~icons/lucide/file
6
7
  var _hoisted_1$6 = {
7
8
  viewBox: "0 0 24 24",
@@ -192,6 +193,8 @@ var GrFilePreview_default = /* @__PURE__ */ defineComponent({
192
193
  tileSize: { default: void 0 },
193
194
  ratio: { default: void 0 },
194
195
  href: { default: void 0 },
196
+ target: { default: void 0 },
197
+ rel: { default: void 0 },
195
198
  as: { default: void 0 },
196
199
  clickable: {
197
200
  type: Boolean,
@@ -204,6 +207,7 @@ var GrFilePreview_default = /* @__PURE__ */ defineComponent({
204
207
  setup(__props, { emit: __emit }) {
205
208
  const props = __props;
206
209
  const emit = __emit;
210
+ const { t } = useGranularityTranslations();
207
211
  const resolvedTileSize = useGrComponentProp("GrFilePreview", "tileSize", () => props.tileSize, "md");
208
212
  const resolvedRatio = useGrComponentProp("GrFilePreview", "ratio", () => props.ratio, "1:1");
209
213
  const resolvedLoading = useGrComponentProp("GrFilePreview", "loading", () => props.loading, "lazy");
@@ -241,11 +245,21 @@ var GrFilePreview_default = /* @__PURE__ */ defineComponent({
241
245
  return props.clickable ? "button" : "div";
242
246
  });
243
247
  /**
248
+ * Ссылка, открывающаяся в новой вкладке, получает защиту без спроса — по факту
249
+ * `_blank`, а не по отдельному пропу: то же правило, что у `GrLink` и
250
+ * `GrButton`. Без `rel` открытая страница получает `window.opener` и может
251
+ * переписать вкладку-источник.
252
+ */
253
+ const resolvedRel = computed(() => props.rel ?? (props.target === "_blank" ? "noopener noreferrer" : void 0));
254
+ /**
244
255
  * Компонент-ссылка (`Link` от Inertia, `RouterLink`) рендерит `<a>` сам, и без
245
- * `href` он ведёт в никуда. Строковый тег, кроме `a`, атрибут не понимает —
246
- * там он и гасится.
256
+ * `href` он ведёт в никуда. Строковый тег, кроме `a`, ссылочных атрибутов не
257
+ * понимает — там они и гасятся.
247
258
  */
248
- const rootHref = computed(() => typeof rootTag.value === "string" && rootTag.value !== "a" ? void 0 : props.href);
259
+ const isAnchorLike = computed(() => typeof rootTag.value !== "string" || rootTag.value === "a");
260
+ const rootHref = computed(() => isAnchorLike.value ? props.href : void 0);
261
+ const rootTarget = computed(() => isAnchorLike.value ? props.target : void 0);
262
+ const rootRel = computed(() => isAnchorLike.value ? resolvedRel.value : void 0);
249
263
  const rootClass = computed(() => [
250
264
  filePreviewRootClass,
251
265
  filePreviewRatioClass[resolvedRatio.value],
@@ -258,6 +272,23 @@ var GrFilePreview_default = /* @__PURE__ */ defineComponent({
258
272
  * декоративна, и придуманное компонентом описание было бы хуже пустого.
259
273
  */
260
274
  const alt = computed(() => props.name ?? "");
275
+ /**
276
+ * Интерактивная плитка без имени не отгружается.
277
+ *
278
+ * Имя ей даёт содержимое: `alt` картинки или подпись заглушки — и то и другое
279
+ * приходит из `name`. Без `name` содержимого нет вовсе: кнопка выходит с пустым
280
+ * `alt`, ссылка — пустой, и axe справедливо зовёт это `button-name` и
281
+ * `link-name`. Родовое имя из локали хуже осмысленного, но лучше пустого —
282
+ * потребителю о промахе говорит гард ниже.
283
+ */
284
+ const resolvedAriaLabel = computed(() => {
285
+ if (props.ariaLabel) return props.ariaLabel;
286
+ if (!isInteractive.value || props.name) return void 0;
287
+ return t("gr.filePreview.label", "File");
288
+ });
289
+ if (process.env.NODE_ENV !== "production") watchEffect(() => {
290
+ if (isInteractive.value && !props.ariaLabel && !props.name) console.warn("[granularity] GrFilePreview: интерактивная плитка без `name` и без `ariaLabel` остаётся без осмысленного имени — скринридер объявит её родовым словом. Передайте имя файла.");
291
+ });
261
292
  function onClick(event) {
262
293
  emit("click", event);
263
294
  }
@@ -267,9 +298,11 @@ var GrFilePreview_default = /* @__PURE__ */ defineComponent({
267
298
  "data-kind": kind.value,
268
299
  type: rootTag.value === "button" ? "button" : void 0,
269
300
  href: rootHref.value,
301
+ target: rootTarget.value,
302
+ rel: rootRel.value,
270
303
  class: normalizeClass(rootClass.value),
271
304
  style: normalizeStyle(rootStyle.value),
272
- "aria-label": __props.ariaLabel,
305
+ "aria-label": resolvedAriaLabel.value,
273
306
  onClick
274
307
  }, {
275
308
  default: withCtx(() => [showSkeleton.value ? (openBlock(), createBlock(GrSkeleton_default, {
@@ -304,6 +337,8 @@ var GrFilePreview_default = /* @__PURE__ */ defineComponent({
304
337
  "data-kind",
305
338
  "type",
306
339
  "href",
340
+ "target",
341
+ "rel",
307
342
  "class",
308
343
  "style",
309
344
  "aria-label"
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GrFilePreview-2kVOg1C7.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)\n return 'unknown'\n\n const type = normalize(mime)\n if (!type)\n return 'unknown'\n\n // Картинка — единственный вид, который компонент рисует, а не подписывает.\n if (type.startsWith('image/'))\n return 'image'\n\n const exact = EXACT[type]\n if (exact)\n return exact\n\n // `text/csv` уже разобран выше: таблица важнее общего правила для текста.\n if (type.startsWith('text/'))\n 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, watchEffect, type Component } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\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 /**\n * Куда открывать ссылку. Объявлен пропом, а не оставлен на fallthrough:\n * иначе `_blank` уезжал бы на `<a>` мимо компонента, и защитный `rel`\n * подставить было бы негде.\n */\n target?: string\n /** Своё значение `rel`. Задано — отменяет автоподстановку. */\n rel?: 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 target: undefined,\n rel: undefined,\n as: undefined,\n clickable: false,\n loading: undefined,\n ariaLabel: undefined,\n})\n\nconst emit = defineEmits<GrFilePreviewEmits>()\n\nconst { t } = useGranularityTranslations()\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, () => {\n imageState.value = 'loading'\n})\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)\n return typeof props.as === 'string' ? props.as : markRaw(props.as)\n if (props.href)\n return 'a'\n return props.clickable ? 'button' : 'div'\n})\n\n/**\n * Ссылка, открывающаяся в новой вкладке, получает защиту без спроса — по факту\n * `_blank`, а не по отдельному пропу: то же правило, что у `GrLink` и\n * `GrButton`. Без `rel` открытая страница получает `window.opener` и может\n * переписать вкладку-источник.\n */\nconst resolvedRel = computed(() => props.rel ?? (props.target === '_blank' ? 'noopener noreferrer' : undefined))\n\n/**\n * Компонент-ссылка (`Link` от Inertia, `RouterLink`) рендерит `<a>` сам, и без\n * `href` он ведёт в никуда. Строковый тег, кроме `a`, ссылочных атрибутов не\n * понимает — там они и гасятся.\n */\nconst isAnchorLike = computed(() => typeof rootTag.value !== 'string' || rootTag.value === 'a')\n\nconst rootHref = computed(() => (isAnchorLike.value ? props.href : undefined))\nconst rootTarget = computed(() => (isAnchorLike.value ? props.target : undefined))\nconst rootRel = computed(() => (isAnchorLike.value ? resolvedRel.value : undefined))\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\n/**\n * Интерактивная плитка без имени не отгружается.\n *\n * Имя ей даёт содержимое: `alt` картинки или подпись заглушки — и то и другое\n * приходит из `name`. Без `name` содержимого нет вовсе: кнопка выходит с пустым\n * `alt`, ссылка — пустой, и axe справедливо зовёт это `button-name` и\n * `link-name`. Родовое имя из локали хуже осмысленного, но лучше пустого —\n * потребителю о промахе говорит гард ниже.\n */\nconst resolvedAriaLabel = computed(() => {\n if (props.ariaLabel)\n return props.ariaLabel\n\n if (!isInteractive.value || props.name)\n return undefined\n\n return t('gr.filePreview.label', 'File')\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (isInteractive.value && !props.ariaLabel && !props.name) {\n console.warn(\n '[granularity] GrFilePreview: интерактивная плитка без `name` и без `ariaLabel` остаётся '\n + 'без осмысленного имени — скринридер объявит её родовым словом. Передайте имя файла.',\n )\n }\n })\n}\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 :target=\"rootTarget\"\n :rel=\"rootRel\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n :aria-label=\"resolvedAriaLabel\"\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, watchEffect, type Component } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\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 /**\n * Куда открывать ссылку. Объявлен пропом, а не оставлен на fallthrough:\n * иначе `_blank` уезжал бы на `<a>` мимо компонента, и защитный `rel`\n * подставить было бы негде.\n */\n target?: string\n /** Своё значение `rel`. Задано — отменяет автоподстановку. */\n rel?: 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 target: undefined,\n rel: undefined,\n as: undefined,\n clickable: false,\n loading: undefined,\n ariaLabel: undefined,\n})\n\nconst emit = defineEmits<GrFilePreviewEmits>()\n\nconst { t } = useGranularityTranslations()\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, () => {\n imageState.value = 'loading'\n})\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)\n return typeof props.as === 'string' ? props.as : markRaw(props.as)\n if (props.href)\n return 'a'\n return props.clickable ? 'button' : 'div'\n})\n\n/**\n * Ссылка, открывающаяся в новой вкладке, получает защиту без спроса — по факту\n * `_blank`, а не по отдельному пропу: то же правило, что у `GrLink` и\n * `GrButton`. Без `rel` открытая страница получает `window.opener` и может\n * переписать вкладку-источник.\n */\nconst resolvedRel = computed(() => props.rel ?? (props.target === '_blank' ? 'noopener noreferrer' : undefined))\n\n/**\n * Компонент-ссылка (`Link` от Inertia, `RouterLink`) рендерит `<a>` сам, и без\n * `href` он ведёт в никуда. Строковый тег, кроме `a`, ссылочных атрибутов не\n * понимает — там они и гасятся.\n */\nconst isAnchorLike = computed(() => typeof rootTag.value !== 'string' || rootTag.value === 'a')\n\nconst rootHref = computed(() => (isAnchorLike.value ? props.href : undefined))\nconst rootTarget = computed(() => (isAnchorLike.value ? props.target : undefined))\nconst rootRel = computed(() => (isAnchorLike.value ? resolvedRel.value : undefined))\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\n/**\n * Интерактивная плитка без имени не отгружается.\n *\n * Имя ей даёт содержимое: `alt` картинки или подпись заглушки — и то и другое\n * приходит из `name`. Без `name` содержимого нет вовсе: кнопка выходит с пустым\n * `alt`, ссылка — пустой, и axe справедливо зовёт это `button-name` и\n * `link-name`. Родовое имя из локали хуже осмысленного, но лучше пустого —\n * потребителю о промахе говорит гард ниже.\n */\nconst resolvedAriaLabel = computed(() => {\n if (props.ariaLabel)\n return props.ariaLabel\n\n if (!isInteractive.value || props.name)\n return undefined\n\n return t('gr.filePreview.label', 'File')\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (isInteractive.value && !props.ariaLabel && !props.name) {\n console.warn(\n '[granularity] GrFilePreview: интерактивная плитка без `name` и без `ariaLabel` остаётся '\n + 'без осмысленного имени — скринридер объявит её родовым словом. Передайте имя файла.',\n )\n }\n })\n}\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 :target=\"rootTarget\"\n :rel=\"rootRel\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n :aria-label=\"resolvedAriaLabel\"\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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"}
@@ -1,3 +1,3 @@
1
- import { n as fileKindOf, r as isPreviewableKind, t as GrFilePreview_default } from "./chunks/GrFilePreview-PabkLgYX.js";
1
+ import { n as fileKindOf, r as isPreviewableKind, t as GrFilePreview_default } from "./chunks/GrFilePreview-2kVOg1C7.js";
2
2
  import { n as grFilePreviewSafelist, t as grFilePreviewConfig } from "../../chunks/config-BaWANzzB.js";
3
3
  export { GrFilePreview_default as GrFilePreview, GrFilePreview_default as default, fileKindOf, grFilePreviewConfig, grFilePreviewSafelist, isPreviewableKind };
@@ -13,7 +13,7 @@ import { t as GrChip_default } from "../../GrChip/chunks/GrChip-CKqyzYcv.js";
13
13
  import { t as loader_2_default } from "../../../chunks/loader-2-B1fdsdWW.js";
14
14
  import { useAnnouncer } from "../../../composables/useAnnouncer.js";
15
15
  import { a as grInputTagWrapperClass, i as grInputTagInputClass, o as spinnerClass, r as clearButtonClass } from "../../../chunks/config-BqT6PMzO.js";
16
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, openBlock, ref, renderList, renderSlot, toDisplayString, unref, withCtx, withModifiers } from "vue";
16
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, openBlock, ref, renderList, renderSlot, toDisplayString, unref, watchEffect, withCtx, withModifiers } from "vue";
17
17
  //#region src/components/GrInputTag/GrInputTag.vue?vue&type=script&setup=true&lang.ts
18
18
  var _hoisted_1 = ["name", "value"];
19
19
  var _hoisted_2 = {
@@ -383,6 +383,9 @@ var GrInputTag_default = /* @__PURE__ */ defineComponent({
383
383
  blur,
384
384
  clear: clearAll
385
385
  });
386
+ if (process.env.NODE_ENV !== "production") watchEffect(() => {
387
+ if (!Array.isArray(props.modelValue)) console.warn(`[granularity] GrInputTag: обязательный проп \`modelValue\` должен быть массивом — получено ${String(props.modelValue)}.`);
388
+ });
386
389
  return (_ctx, _cache) => {
387
390
  return openBlock(), createElementBlock("div", {
388
391
  ref_key: "rootEl",
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GrInputTag-BKzm1PKw.js","names":[],"sources":["../../../../src/components/GrInputTag/GrInputTag.vue","../../../../src/components/GrInputTag/GrInputTag.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watchEffect } from 'vue'\n\nimport type { GrBadgeRadius, GrBadgeSize, GrBadgeTone } from '../GrBadge'\nimport GrChip from '../GrChip/GrChip.vue'\nimport { chipSizeForBadgeScale } from '../GrChip/grChipStyles'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport IconClose from '~icons/lucide/x'\nimport IconLoader from '~icons/lucide/loader-2'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { isComposingEvent } from '../../internal/keyboard'\n\nimport {\n clearButtonClass,\n grInputTagInputClass,\n grInputTagWrapperClass,\n spinnerClass,\n type GrInputTagSize,\n type GrInputTagState,\n} from './grInputTagStyles'\n\nexport type { GrInputTagSize, GrInputTagState } from './grInputTagStyles'\n\nconst REGEX_SPECIAL_CHAR_RE = /[.*+?^${}()|[\\]\\\\]/g\n\n/**\n * Props for {@link GrInputTag}.\n * Tag-input primitive: renders a set of badges with an inline `<input>` for entering new tags.\n * Splits input by configurable `separators`, supports paste, Enter and Backspace editing.\n */\nexport interface GrInputTagProps {\n modelValue: string[]\n placeholder?: string\n disabled?: boolean\n readonly?: boolean\n invalid?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n state?: GrInputTagState\n size?: GrInputTagSize\n separators?: string[]\n allowDuplicates?: boolean\n trim?: boolean\n max?: number\n addOnBlur?: boolean\n clearInputOnAdd?: boolean\n /**\n * Проверка тега перед добавлением. Может быть асинхронной (проверка на\n * сервере). Отклонённый тег не добавляется и уходит в событие `reject`.\n */\n beforeAdd?: (tag: string) => boolean | Promise<boolean>\n /** Кнопка «снести все теги». Настраивается через `GrConfigProvider`. */\n clearable?: boolean\n /** Фоновая работа: спиннер + `aria-busy`. Асинхронный `beforeAdd` поднимает его сам. */\n loading?: boolean\n tagTone?: GrBadgeTone\n tagDark?: boolean\n tagSize?: GrBadgeSize\n tagRadius?: GrBadgeRadius\n tagClosable?: boolean\n /** i18n-friendly aria-label for the per-tag remove button. */\n removeTagLabel?: string\n /** i18n aria-label кнопки «снести все». */\n clearAllLabel?: string\n /**\n * Имя контрола, когда он используется вне `GrFormField`. Внутри поля имя даёт\n * связка с его `<label for>` — там проп не нужен. Без того и другого у инпута\n * нет доступного имени вовсе: placeholder именем не считается.\n */\n ariaLabel?: string\n /** Имя для нативной формы: hidden input на каждый тег. */\n name?: string\n /**\n * Ширины аддонов `prefix`/`suffix` — общий контракт контролов пакета\n * (`docs/form-controls.md`).\n */\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n prefixFixed?: boolean\n suffixFixed?: boolean\n}\n\nexport interface GrInputTagEmits {\n (e: 'update:modelValue', value: string[]): void\n (e: 'add', value: string): void\n (e: 'remove', value: string, index: number): void\n /** Тег не прошёл `beforeAdd`. */\n (e: 'reject', value: string): void\n /** Набор снесён кнопкой «очистить». */\n (e: 'clear'): void\n /** Набор тегов изменился. */\n (e: 'change', value: string[]): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrInputTagProps>(),\n {\n placeholder: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n state: 'default',\n // Настраивается через `GrConfigProvider`; дефолт — в резолвере ниже.\n size: undefined,\n separators: () => [','],\n allowDuplicates: false,\n trim: true,\n max: undefined,\n addOnBlur: false,\n clearInputOnAdd: true,\n beforeAdd: undefined,\n clearable: undefined,\n loading: false,\n tagTone: 'neutral',\n tagDark: false,\n tagSize: 'md',\n tagRadius: 'round',\n tagClosable: true,\n removeTagLabel: undefined,\n clearAllLabel: undefined,\n ariaLabel: undefined,\n name: undefined,\n },\n)\n\nconst emit = defineEmits<GrInputTagEmits>()\ndefineSlots<{\n /** Аддон слева от чипов: иконка, метка. */\n prefix?: () => any\n /** Аддон справа, перед кнопкой очистки. */\n suffix?: () => any\n /** Свой чип: `remove` снимает значение. */\n tag?: (props: { tag: string, index: number, remove: () => void }) => any\n}>()\n\n// Fallback из контекста `GrFormField` (id/aria-describedby/invalid/required) —\n// как у GrInput и GrSelect: контрол не знает про форму, знает только про поле.\nconst field = useGrFormFieldContext()\nconst resolvedId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\n// Эффективные значения: локальный проп → `GrConfigProvider` → дефолт компонента.\n/** Ширина аддона по умолчанию — та же лестница, что у `GrInput`. */\nconst ADDON_MIN_WIDTH_BY_SIZE: Record<GrInputTagSize, string> = {\n xs: '2rem',\n sm: '2.25rem',\n md: '2.5rem',\n lg: '3rem',\n}\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrInputTag' })\n\n/**\n * Аддоны — флекс-соседи поля: оболочка тегового поля и так флекс-строка с\n * бейджами, поэтому места паддингом резервировать не нужно.\n */\nconst { hasPrefix, hasSuffix, prefixEl, suffixEl, prefixStyle, suffixStyle } = useControlAddons(() => props, {\n defaultMinWidth: () => ADDON_MIN_WIDTH_BY_SIZE[resolvedSize.value],\n paddingX: () => '0px',\n})\nconst resolvedClearable = useGrComponentProp('GrInputTag', 'clearable', () => props.clearable, false)\n\nconst { t } = useGranularityTranslations()\nconst resolvedClearAllLabel = computed(() => props.clearAllLabel ?? t('gr.inputTag.clearAll', 'Clear all tags'))\n\nfunction removeTagLabelFor(tag: string): string {\n // Безликое «Remove tag» на двадцати кнопках подряд не даёт выбрать нужную:\n // имя обязано называть сам тег.\n return props.removeTagLabel ?? t('gr.inputTag.removeTagNamed', 'Remove tag {tag}', { tag })\n}\n\nconst inputValue = ref('')\nconst inputEl = ref<HTMLInputElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Внутри корня фокус ходит между полем и кнопками удаления чипов: без границы\n// каждое такое перемещение давало бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n/**\n * Инстансы чипов: у кольца цель — кнопка снятия внутри чипа, и достаётся она\n * через `defineExpose` (`removeEl`), а не селектором по DOM.\n */\nconst chipRefs = ref<Array<{ removeEl: HTMLButtonElement | null } | null>>([])\n\nfunction setChipRef(index: number) {\n return (el: unknown): void => {\n chipRefs.value[index] = el as { removeEl: HTMLButtonElement | null } | null\n }\n}\n\n/**\n * Проп `tagSize` объявлен по шкале бейджа и остаётся публичным контрактом:\n * `md` обязан означать для потребителя тот же кегль, что и раньше.\n */\nconst chipSize = computed(() => chipSizeForBadgeScale(props.tagSize))\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\nconst isMaxed = computed(() => props.max !== undefined && props.modelValue.length >= props.max)\nconst canEdit = computed(() => !isDisabled.value && !isReadonly.value)\nconst showRemove = computed(() => props.tagClosable && canEdit.value)\n\n// Добавление, удаление и достигнутый предел — события, а не состояние виджета:\n// показывать их негде, объявить нужно. Это работа общего живого региона.\nconst { announce } = useAnnouncer()\n\n/**\n * Roving tabindex по крестикам: двадцать тегов давали двадцать одну остановку\n * `Tab`. В таб-порядке ровно один крестик, между ними ходят стрелками.\n *\n * Адресация индексом, а не значением тега: теги допускают повторы, и по\n * значению два одинаковых чипа было бы не различить.\n */\nconst tagIndexes = computed(() => props.modelValue.map((_, index) => index))\n\nconst roving = useRovingFocus<number>({\n items: () => tagIndexes.value,\n elementFor: index => chipRefs.value[index]?.removeEl,\n orientation: () => 'horizontal',\n // Ряд чипов — не кольцо: у него есть край, и за краем стоит поле ввода.\n wrap: () => false,\n onOverflow: (edge) => {\n // За последним чипом — поле ввода: продолжение того же ряда. Слева от\n // первого — начало строки, фокус остаётся на месте.\n if (edge === 'end')\n focus()\n return true\n },\n // Удаление чипа перерисовывает ряд: без ожидания фокус уехал бы на узел,\n // которого уже нет.\n beforeFocus: () => nextTick(),\n})\n\nasync function focusTag(index: number): Promise<void> {\n const last = props.modelValue.length - 1\n if (last < 0) {\n focus()\n return\n }\n\n await roving.focusKey(Math.min(Math.max(index, 0), last))\n}\n\nfunction escapeRegexChar(ch: string): string {\n return ch.replace(REGEX_SPECIAL_CHAR_RE, '\\\\$&')\n}\n\nconst splitRegex = computed(() => {\n const seps = (props.separators ?? []).filter(Boolean).map(s => s[0]!)\n const uniq = [...new Set([...seps, '\\n', '\\r', '\\t'])]\n const body = uniq.map(escapeRegexChar).join('')\n return new RegExp(`[${body}]+`, 'g')\n})\n\nfunction normalizeTag(raw: string): string {\n return props.trim ? raw.trim() : raw\n}\n\nfunction splitToTags(raw: string): string[] {\n return raw\n .split(splitRegex.value)\n .map(v => normalizeTag(v))\n .filter(Boolean)\n}\n\nfunction addMany(rawTags: string[]): boolean {\n if (!canEdit.value || isMaxed.value)\n return false\n\n const next = props.modelValue.slice()\n const added: string[] = []\n\n for (const raw of rawTags) {\n const tag = normalizeTag(raw)\n if (!tag)\n continue\n\n if (!props.allowDuplicates && next.includes(tag))\n continue\n\n if (props.max !== undefined && next.length >= props.max)\n break\n\n next.push(tag)\n added.push(tag)\n emit('add', tag)\n }\n\n if (!added.length)\n return false\n\n emit('update:modelValue', next)\n emit('change', next)\n announce(added.length === 1\n ? t('gr.inputTag.added', 'Tag added: {tag}', { tag: added[0]! })\n : t('gr.inputTag.addedMany', '{count} tags added', { count: added.length }))\n\n return true\n}\n\n// Счётчик запуска: пока идёт асинхронная проверка, пользователь успевает нажать\n// Enter второй раз — результат устаревшей проверки дописывать нельзя.\nlet validationRun = 0\nconst validating = ref(false)\nconst isBusy = computed(() => props.loading || validating.value)\n\nasync function filterAllowed(tags: string[], run: number): Promise<string[] | null> {\n const check = props.beforeAdd\n if (!check)\n return tags\n\n const allowed: string[] = []\n\n for (const tag of tags) {\n const ok = await check(tag)\n if (run !== validationRun)\n return null\n\n if (ok)\n allowed.push(tag)\n else\n emit('reject', tag)\n }\n\n return allowed\n}\n\nasync function commitInput(): Promise<void> {\n if (!canEdit.value || !inputValue.value)\n return\n\n if (isMaxed.value) {\n announce(t('gr.inputTag.limitReached', 'Tag limit reached'))\n return\n }\n\n const raw = inputValue.value\n const tags = splitToTags(raw)\n if (!tags.length)\n return\n\n const run = ++validationRun\n validating.value = Boolean(props.beforeAdd)\n\n const allowed = await filterAllowed(tags, run)\n\n if (run === validationRun)\n validating.value = false\n\n if (allowed === null || !allowed.length)\n return\n\n const changed = addMany(allowed)\n // Значение могло измениться, пока шла проверка: стирать чужой ввод нельзя.\n if (changed && props.clearInputOnAdd && inputValue.value === raw)\n inputValue.value = ''\n}\n\nfunction removeAt(index: number): void {\n if (!canEdit.value)\n return\n\n const next = props.modelValue.slice()\n const removed = next[index]\n if (removed === undefined)\n return\n\n next.splice(index, 1)\n emit('update:modelValue', next)\n emit('change', next)\n emit('remove', removed, index)\n announce(t('gr.inputTag.removed', 'Tag removed: {tag}', { tag: removed }))\n}\n\n/** Удаление с клавиатуры: фокус переезжает на соседний чип, а не пропадает. */\nasync function removeAtFromKeyboard(index: number): Promise<void> {\n removeAt(index)\n await nextTick()\n\n if (props.modelValue.length === 0) {\n focus()\n return\n }\n\n await focusTag(Math.min(index, props.modelValue.length - 1))\n}\n\nfunction clearAll(): void {\n if (!canEdit.value || props.modelValue.length === 0)\n return\n\n emit('update:modelValue', [])\n emit('change', [])\n emit('clear')\n announce(t('gr.inputTag.cleared', 'All tags removed'))\n focus()\n}\n\nfunction onInput(e: Event): void {\n inputValue.value = (e.target as HTMLInputElement).value\n}\n\nfunction onKeydown(e: KeyboardEvent): void {\n // Enter/сепаратор во время IME-композиции коммитят её, а не тег.\n if (isComposingEvent(e))\n return\n if (!canEdit.value)\n return\n\n if (e.key === 'Enter') {\n e.preventDefault()\n void commitInput()\n return\n }\n\n if ((props.separators ?? []).includes(e.key)) {\n e.preventDefault()\n void commitInput()\n return\n }\n\n if (inputValue.value.length === 0 && props.modelValue.length > 0) {\n if (e.key === 'Backspace') {\n e.preventDefault()\n removeAt(props.modelValue.length - 1)\n return\n }\n\n // Из пустого поля стрелка влево уводит на последний чип — дальше по чипам\n // работают те же стрелки, `Tab` для этого не нужен.\n if (e.key === 'ArrowLeft') {\n e.preventDefault()\n void focusTag(props.modelValue.length - 1)\n }\n }\n}\n\nfunction onTagKeydown(e: KeyboardEvent, index: number): void {\n if (roving.handleNavigationKeys(e)) {\n return\n }\n\n if (e.key === 'Delete' || e.key === 'Backspace') {\n e.preventDefault()\n void removeAtFromKeyboard(index)\n }\n}\n\nfunction onBlur(): void {\n if (props.addOnBlur)\n void commitInput()\n}\n\nfunction onPaste(e: ClipboardEvent): void {\n if (!canEdit.value || isMaxed.value)\n return\n\n const pasted = e.clipboardData?.getData('text')\n if (!pasted)\n return\n\n const tags = splitToTags(pasted)\n if (tags.length <= 1)\n return\n\n e.preventDefault()\n\n const run = ++validationRun\n validating.value = Boolean(props.beforeAdd)\n\n void filterAllowed(tags, run).then((allowed) => {\n if (run === validationRun)\n validating.value = false\n\n if (allowed?.length)\n addMany(allowed)\n })\n}\n\nconst wrapperClassName = computed(() => {\n return grInputTagWrapperClass({\n size: resolvedSize.value,\n state: props.state,\n invalid: isInvalid.value,\n disabled: isDisabled.value,\n })\n})\n\nconst inputClassName = computed(() => grInputTagInputClass(resolvedSize.value))\n\nconst showClearAll = computed(() => resolvedClearable.value && canEdit.value && props.modelValue.length > 0)\n\nconst placeholderText = computed(() => props.modelValue.length > 0 ? undefined : props.placeholder)\n\ndefineExpose({ focus, blur, clear: clearAll })\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (!Array.isArray(props.modelValue)) {\n console.warn(\n `[granularity] GrInputTag: обязательный проп \\`modelValue\\` должен быть массивом — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-input-tag\n data-testid=\"gr-input-tag\"\n :class=\"wrapperClassName\"\n @click=\"focus\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <span\n v-if=\"hasPrefix\"\n ref=\"prefixEl\"\n data-gr-input-tag-prefix\n class=\"inline-flex shrink-0 items-center justify-center text-[var(--gr-muted-fg)]\"\n :style=\"prefixStyle\"\n >\n <slot name=\"prefix\" />\n </span>\n\n <!-- Нативная форма: hidden на каждый тег — стандартная сериализация набора. -->\n <template v-if=\"name\">\n <input\n v-for=\"(tag, tagIndex) in modelValue\"\n :key=\"`hidden-${tagIndex}`\"\n type=\"hidden\"\n :name=\"name\"\n :value=\"tag\"\n >\n </template>\n <!--\n `display: contents` — список нужен ради роли, а не ради раскладки: чипы\n обязаны переноситься в одном потоке с полем ввода.\n -->\n <span v-if=\"modelValue.length\" role=\"list\" class=\"contents\">\n <GrChip\n v-for=\"(tag, i) in modelValue\"\n :key=\"`${tag}-${i}`\"\n :ref=\"setChipRef(i)\"\n role=\"listitem\"\n :tone=\"tagTone\"\n :dark=\"tagDark\"\n :size=\"chipSize\"\n :radius=\"tagRadius\"\n :closable=\"showRemove\"\n :remove-label=\"removeTagLabelFor(tag)\"\n :remove-tabindex=\"roving.tabindexFor(i)\"\n data-gr-input-tag-item\n data-testid=\"gr-input-tag-item\"\n :data-index=\"i\"\n @remove=\"removeAt(i)\"\n @focusin=\"roving.setActive(i)\"\n @keydown=\"onTagKeydown($event, i)\"\n >\n <slot name=\"tag\" :tag=\"tag\" :index=\"i\" :remove=\"() => removeAt(i)\">\n <span class=\"truncate max-w-[18rem]\">{{ tag }}</span>\n </slot>\n </GrChip>\n </span>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n data-gr-input-tag-input\n data-testid=\"gr-input-tag-input\"\n :value=\"inputValue\"\n :disabled=\"isDisabled\"\n :readonly=\"isReadonly\"\n :placeholder=\"placeholderText\"\n :aria-label=\"ariaLabel\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-busy=\"isBusy ? 'true' : undefined\"\n class=\"flex-1 min-w-[120px] bg-transparent border-none outline-none placeholder:text-[var(--gr-muted-fg)]\"\n :class=\"inputClassName\"\n @input=\"onInput\"\n @keydown=\"onKeydown\"\n @blur=\"onBlur\"\n @paste=\"onPaste\"\n >\n\n <span\n v-if=\"hasSuffix\"\n ref=\"suffixEl\"\n data-gr-input-tag-suffix\n class=\"inline-flex shrink-0 items-center justify-center text-[var(--gr-muted-fg)]\"\n :style=\"suffixStyle\"\n >\n <slot name=\"suffix\" />\n </span>\n\n <span\n v-if=\"isBusy\"\n data-gr-input-tag-spinner\n :class=\"spinnerClass\"\n aria-hidden=\"true\"\n >\n <GrIcon size=\"sm\">\n <IconLoader class=\"animate-spin\" />\n </GrIcon>\n </span>\n\n <button\n v-if=\"showClearAll\"\n type=\"button\"\n data-gr-input-tag-clear\n :class=\"clearButtonClass\"\n :aria-label=\"resolvedClearAllLabel\"\n @mousedown.prevent.stop\n @click.stop=\"clearAll\"\n >\n <GrIcon size=\"sm\">\n <IconClose />\n </GrIcon>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watchEffect } from 'vue'\n\nimport type { GrBadgeRadius, GrBadgeSize, GrBadgeTone } from '../GrBadge'\nimport GrChip from '../GrChip/GrChip.vue'\nimport { chipSizeForBadgeScale } from '../GrChip/grChipStyles'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport IconClose from '~icons/lucide/x'\nimport IconLoader from '~icons/lucide/loader-2'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { isComposingEvent } from '../../internal/keyboard'\n\nimport {\n clearButtonClass,\n grInputTagInputClass,\n grInputTagWrapperClass,\n spinnerClass,\n type GrInputTagSize,\n type GrInputTagState,\n} from './grInputTagStyles'\n\nexport type { GrInputTagSize, GrInputTagState } from './grInputTagStyles'\n\nconst REGEX_SPECIAL_CHAR_RE = /[.*+?^${}()|[\\]\\\\]/g\n\n/**\n * Props for {@link GrInputTag}.\n * Tag-input primitive: renders a set of badges with an inline `<input>` for entering new tags.\n * Splits input by configurable `separators`, supports paste, Enter and Backspace editing.\n */\nexport interface GrInputTagProps {\n modelValue: string[]\n placeholder?: string\n disabled?: boolean\n readonly?: boolean\n invalid?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n state?: GrInputTagState\n size?: GrInputTagSize\n separators?: string[]\n allowDuplicates?: boolean\n trim?: boolean\n max?: number\n addOnBlur?: boolean\n clearInputOnAdd?: boolean\n /**\n * Проверка тега перед добавлением. Может быть асинхронной (проверка на\n * сервере). Отклонённый тег не добавляется и уходит в событие `reject`.\n */\n beforeAdd?: (tag: string) => boolean | Promise<boolean>\n /** Кнопка «снести все теги». Настраивается через `GrConfigProvider`. */\n clearable?: boolean\n /** Фоновая работа: спиннер + `aria-busy`. Асинхронный `beforeAdd` поднимает его сам. */\n loading?: boolean\n tagTone?: GrBadgeTone\n tagDark?: boolean\n tagSize?: GrBadgeSize\n tagRadius?: GrBadgeRadius\n tagClosable?: boolean\n /** i18n-friendly aria-label for the per-tag remove button. */\n removeTagLabel?: string\n /** i18n aria-label кнопки «снести все». */\n clearAllLabel?: string\n /**\n * Имя контрола, когда он используется вне `GrFormField`. Внутри поля имя даёт\n * связка с его `<label for>` — там проп не нужен. Без того и другого у инпута\n * нет доступного имени вовсе: placeholder именем не считается.\n */\n ariaLabel?: string\n /** Имя для нативной формы: hidden input на каждый тег. */\n name?: string\n /**\n * Ширины аддонов `prefix`/`suffix` — общий контракт контролов пакета\n * (`docs/form-controls.md`).\n */\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n prefixFixed?: boolean\n suffixFixed?: boolean\n}\n\nexport interface GrInputTagEmits {\n (e: 'update:modelValue', value: string[]): void\n (e: 'add', value: string): void\n (e: 'remove', value: string, index: number): void\n /** Тег не прошёл `beforeAdd`. */\n (e: 'reject', value: string): void\n /** Набор снесён кнопкой «очистить». */\n (e: 'clear'): void\n /** Набор тегов изменился. */\n (e: 'change', value: string[]): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrInputTagProps>(),\n {\n placeholder: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n state: 'default',\n // Настраивается через `GrConfigProvider`; дефолт — в резолвере ниже.\n size: undefined,\n separators: () => [','],\n allowDuplicates: false,\n trim: true,\n max: undefined,\n addOnBlur: false,\n clearInputOnAdd: true,\n beforeAdd: undefined,\n clearable: undefined,\n loading: false,\n tagTone: 'neutral',\n tagDark: false,\n tagSize: 'md',\n tagRadius: 'round',\n tagClosable: true,\n removeTagLabel: undefined,\n clearAllLabel: undefined,\n ariaLabel: undefined,\n name: undefined,\n },\n)\n\nconst emit = defineEmits<GrInputTagEmits>()\ndefineSlots<{\n /** Аддон слева от чипов: иконка, метка. */\n prefix?: () => any\n /** Аддон справа, перед кнопкой очистки. */\n suffix?: () => any\n /** Свой чип: `remove` снимает значение. */\n tag?: (props: { tag: string, index: number, remove: () => void }) => any\n}>()\n\n// Fallback из контекста `GrFormField` (id/aria-describedby/invalid/required) —\n// как у GrInput и GrSelect: контрол не знает про форму, знает только про поле.\nconst field = useGrFormFieldContext()\nconst resolvedId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\n// Эффективные значения: локальный проп → `GrConfigProvider` → дефолт компонента.\n/** Ширина аддона по умолчанию — та же лестница, что у `GrInput`. */\nconst ADDON_MIN_WIDTH_BY_SIZE: Record<GrInputTagSize, string> = {\n xs: '2rem',\n sm: '2.25rem',\n md: '2.5rem',\n lg: '3rem',\n}\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrInputTag' })\n\n/**\n * Аддоны — флекс-соседи поля: оболочка тегового поля и так флекс-строка с\n * бейджами, поэтому места паддингом резервировать не нужно.\n */\nconst { hasPrefix, hasSuffix, prefixEl, suffixEl, prefixStyle, suffixStyle } = useControlAddons(() => props, {\n defaultMinWidth: () => ADDON_MIN_WIDTH_BY_SIZE[resolvedSize.value],\n paddingX: () => '0px',\n})\nconst resolvedClearable = useGrComponentProp('GrInputTag', 'clearable', () => props.clearable, false)\n\nconst { t } = useGranularityTranslations()\nconst resolvedClearAllLabel = computed(() => props.clearAllLabel ?? t('gr.inputTag.clearAll', 'Clear all tags'))\n\nfunction removeTagLabelFor(tag: string): string {\n // Безликое «Remove tag» на двадцати кнопках подряд не даёт выбрать нужную:\n // имя обязано называть сам тег.\n return props.removeTagLabel ?? t('gr.inputTag.removeTagNamed', 'Remove tag {tag}', { tag })\n}\n\nconst inputValue = ref('')\nconst inputEl = ref<HTMLInputElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Внутри корня фокус ходит между полем и кнопками удаления чипов: без границы\n// каждое такое перемещение давало бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n/**\n * Инстансы чипов: у кольца цель — кнопка снятия внутри чипа, и достаётся она\n * через `defineExpose` (`removeEl`), а не селектором по DOM.\n */\nconst chipRefs = ref<Array<{ removeEl: HTMLButtonElement | null } | null>>([])\n\nfunction setChipRef(index: number) {\n return (el: unknown): void => {\n chipRefs.value[index] = el as { removeEl: HTMLButtonElement | null } | null\n }\n}\n\n/**\n * Проп `tagSize` объявлен по шкале бейджа и остаётся публичным контрактом:\n * `md` обязан означать для потребителя тот же кегль, что и раньше.\n */\nconst chipSize = computed(() => chipSizeForBadgeScale(props.tagSize))\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\nconst isMaxed = computed(() => props.max !== undefined && props.modelValue.length >= props.max)\nconst canEdit = computed(() => !isDisabled.value && !isReadonly.value)\nconst showRemove = computed(() => props.tagClosable && canEdit.value)\n\n// Добавление, удаление и достигнутый предел — события, а не состояние виджета:\n// показывать их негде, объявить нужно. Это работа общего живого региона.\nconst { announce } = useAnnouncer()\n\n/**\n * Roving tabindex по крестикам: двадцать тегов давали двадцать одну остановку\n * `Tab`. В таб-порядке ровно один крестик, между ними ходят стрелками.\n *\n * Адресация индексом, а не значением тега: теги допускают повторы, и по\n * значению два одинаковых чипа было бы не различить.\n */\nconst tagIndexes = computed(() => props.modelValue.map((_, index) => index))\n\nconst roving = useRovingFocus<number>({\n items: () => tagIndexes.value,\n elementFor: index => chipRefs.value[index]?.removeEl,\n orientation: () => 'horizontal',\n // Ряд чипов — не кольцо: у него есть край, и за краем стоит поле ввода.\n wrap: () => false,\n onOverflow: (edge) => {\n // За последним чипом — поле ввода: продолжение того же ряда. Слева от\n // первого — начало строки, фокус остаётся на месте.\n if (edge === 'end')\n focus()\n return true\n },\n // Удаление чипа перерисовывает ряд: без ожидания фокус уехал бы на узел,\n // которого уже нет.\n beforeFocus: () => nextTick(),\n})\n\nasync function focusTag(index: number): Promise<void> {\n const last = props.modelValue.length - 1\n if (last < 0) {\n focus()\n return\n }\n\n await roving.focusKey(Math.min(Math.max(index, 0), last))\n}\n\nfunction escapeRegexChar(ch: string): string {\n return ch.replace(REGEX_SPECIAL_CHAR_RE, '\\\\$&')\n}\n\nconst splitRegex = computed(() => {\n const seps = (props.separators ?? []).filter(Boolean).map(s => s[0]!)\n const uniq = [...new Set([...seps, '\\n', '\\r', '\\t'])]\n const body = uniq.map(escapeRegexChar).join('')\n return new RegExp(`[${body}]+`, 'g')\n})\n\nfunction normalizeTag(raw: string): string {\n return props.trim ? raw.trim() : raw\n}\n\nfunction splitToTags(raw: string): string[] {\n return raw\n .split(splitRegex.value)\n .map(v => normalizeTag(v))\n .filter(Boolean)\n}\n\nfunction addMany(rawTags: string[]): boolean {\n if (!canEdit.value || isMaxed.value)\n return false\n\n const next = props.modelValue.slice()\n const added: string[] = []\n\n for (const raw of rawTags) {\n const tag = normalizeTag(raw)\n if (!tag)\n continue\n\n if (!props.allowDuplicates && next.includes(tag))\n continue\n\n if (props.max !== undefined && next.length >= props.max)\n break\n\n next.push(tag)\n added.push(tag)\n emit('add', tag)\n }\n\n if (!added.length)\n return false\n\n emit('update:modelValue', next)\n emit('change', next)\n announce(added.length === 1\n ? t('gr.inputTag.added', 'Tag added: {tag}', { tag: added[0]! })\n : t('gr.inputTag.addedMany', '{count} tags added', { count: added.length }))\n\n return true\n}\n\n// Счётчик запуска: пока идёт асинхронная проверка, пользователь успевает нажать\n// Enter второй раз — результат устаревшей проверки дописывать нельзя.\nlet validationRun = 0\nconst validating = ref(false)\nconst isBusy = computed(() => props.loading || validating.value)\n\nasync function filterAllowed(tags: string[], run: number): Promise<string[] | null> {\n const check = props.beforeAdd\n if (!check)\n return tags\n\n const allowed: string[] = []\n\n for (const tag of tags) {\n const ok = await check(tag)\n if (run !== validationRun)\n return null\n\n if (ok)\n allowed.push(tag)\n else\n emit('reject', tag)\n }\n\n return allowed\n}\n\nasync function commitInput(): Promise<void> {\n if (!canEdit.value || !inputValue.value)\n return\n\n if (isMaxed.value) {\n announce(t('gr.inputTag.limitReached', 'Tag limit reached'))\n return\n }\n\n const raw = inputValue.value\n const tags = splitToTags(raw)\n if (!tags.length)\n return\n\n const run = ++validationRun\n validating.value = Boolean(props.beforeAdd)\n\n const allowed = await filterAllowed(tags, run)\n\n if (run === validationRun)\n validating.value = false\n\n if (allowed === null || !allowed.length)\n return\n\n const changed = addMany(allowed)\n // Значение могло измениться, пока шла проверка: стирать чужой ввод нельзя.\n if (changed && props.clearInputOnAdd && inputValue.value === raw)\n inputValue.value = ''\n}\n\nfunction removeAt(index: number): void {\n if (!canEdit.value)\n return\n\n const next = props.modelValue.slice()\n const removed = next[index]\n if (removed === undefined)\n return\n\n next.splice(index, 1)\n emit('update:modelValue', next)\n emit('change', next)\n emit('remove', removed, index)\n announce(t('gr.inputTag.removed', 'Tag removed: {tag}', { tag: removed }))\n}\n\n/** Удаление с клавиатуры: фокус переезжает на соседний чип, а не пропадает. */\nasync function removeAtFromKeyboard(index: number): Promise<void> {\n removeAt(index)\n await nextTick()\n\n if (props.modelValue.length === 0) {\n focus()\n return\n }\n\n await focusTag(Math.min(index, props.modelValue.length - 1))\n}\n\nfunction clearAll(): void {\n if (!canEdit.value || props.modelValue.length === 0)\n return\n\n emit('update:modelValue', [])\n emit('change', [])\n emit('clear')\n announce(t('gr.inputTag.cleared', 'All tags removed'))\n focus()\n}\n\nfunction onInput(e: Event): void {\n inputValue.value = (e.target as HTMLInputElement).value\n}\n\nfunction onKeydown(e: KeyboardEvent): void {\n // Enter/сепаратор во время IME-композиции коммитят её, а не тег.\n if (isComposingEvent(e))\n return\n if (!canEdit.value)\n return\n\n if (e.key === 'Enter') {\n e.preventDefault()\n void commitInput()\n return\n }\n\n if ((props.separators ?? []).includes(e.key)) {\n e.preventDefault()\n void commitInput()\n return\n }\n\n if (inputValue.value.length === 0 && props.modelValue.length > 0) {\n if (e.key === 'Backspace') {\n e.preventDefault()\n removeAt(props.modelValue.length - 1)\n return\n }\n\n // Из пустого поля стрелка влево уводит на последний чип — дальше по чипам\n // работают те же стрелки, `Tab` для этого не нужен.\n if (e.key === 'ArrowLeft') {\n e.preventDefault()\n void focusTag(props.modelValue.length - 1)\n }\n }\n}\n\nfunction onTagKeydown(e: KeyboardEvent, index: number): void {\n if (roving.handleNavigationKeys(e)) {\n return\n }\n\n if (e.key === 'Delete' || e.key === 'Backspace') {\n e.preventDefault()\n void removeAtFromKeyboard(index)\n }\n}\n\nfunction onBlur(): void {\n if (props.addOnBlur)\n void commitInput()\n}\n\nfunction onPaste(e: ClipboardEvent): void {\n if (!canEdit.value || isMaxed.value)\n return\n\n const pasted = e.clipboardData?.getData('text')\n if (!pasted)\n return\n\n const tags = splitToTags(pasted)\n if (tags.length <= 1)\n return\n\n e.preventDefault()\n\n const run = ++validationRun\n validating.value = Boolean(props.beforeAdd)\n\n void filterAllowed(tags, run).then((allowed) => {\n if (run === validationRun)\n validating.value = false\n\n if (allowed?.length)\n addMany(allowed)\n })\n}\n\nconst wrapperClassName = computed(() => {\n return grInputTagWrapperClass({\n size: resolvedSize.value,\n state: props.state,\n invalid: isInvalid.value,\n disabled: isDisabled.value,\n })\n})\n\nconst inputClassName = computed(() => grInputTagInputClass(resolvedSize.value))\n\nconst showClearAll = computed(() => resolvedClearable.value && canEdit.value && props.modelValue.length > 0)\n\nconst placeholderText = computed(() => props.modelValue.length > 0 ? undefined : props.placeholder)\n\ndefineExpose({ focus, blur, clear: clearAll })\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (!Array.isArray(props.modelValue)) {\n console.warn(\n `[granularity] GrInputTag: обязательный проп \\`modelValue\\` должен быть массивом — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-input-tag\n data-testid=\"gr-input-tag\"\n :class=\"wrapperClassName\"\n @click=\"focus\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <span\n v-if=\"hasPrefix\"\n ref=\"prefixEl\"\n data-gr-input-tag-prefix\n class=\"inline-flex shrink-0 items-center justify-center text-[var(--gr-muted-fg)]\"\n :style=\"prefixStyle\"\n >\n <slot name=\"prefix\" />\n </span>\n\n <!-- Нативная форма: hidden на каждый тег — стандартная сериализация набора. -->\n <template v-if=\"name\">\n <input\n v-for=\"(tag, tagIndex) in modelValue\"\n :key=\"`hidden-${tagIndex}`\"\n type=\"hidden\"\n :name=\"name\"\n :value=\"tag\"\n >\n </template>\n <!--\n `display: contents` — список нужен ради роли, а не ради раскладки: чипы\n обязаны переноситься в одном потоке с полем ввода.\n -->\n <span v-if=\"modelValue.length\" role=\"list\" class=\"contents\">\n <GrChip\n v-for=\"(tag, i) in modelValue\"\n :key=\"`${tag}-${i}`\"\n :ref=\"setChipRef(i)\"\n role=\"listitem\"\n :tone=\"tagTone\"\n :dark=\"tagDark\"\n :size=\"chipSize\"\n :radius=\"tagRadius\"\n :closable=\"showRemove\"\n :remove-label=\"removeTagLabelFor(tag)\"\n :remove-tabindex=\"roving.tabindexFor(i)\"\n data-gr-input-tag-item\n data-testid=\"gr-input-tag-item\"\n :data-index=\"i\"\n @remove=\"removeAt(i)\"\n @focusin=\"roving.setActive(i)\"\n @keydown=\"onTagKeydown($event, i)\"\n >\n <slot name=\"tag\" :tag=\"tag\" :index=\"i\" :remove=\"() => removeAt(i)\">\n <span class=\"truncate max-w-[18rem]\">{{ tag }}</span>\n </slot>\n </GrChip>\n </span>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n data-gr-input-tag-input\n data-testid=\"gr-input-tag-input\"\n :value=\"inputValue\"\n :disabled=\"isDisabled\"\n :readonly=\"isReadonly\"\n :placeholder=\"placeholderText\"\n :aria-label=\"ariaLabel\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-busy=\"isBusy ? 'true' : undefined\"\n class=\"flex-1 min-w-[120px] bg-transparent border-none outline-none placeholder:text-[var(--gr-muted-fg)]\"\n :class=\"inputClassName\"\n @input=\"onInput\"\n @keydown=\"onKeydown\"\n @blur=\"onBlur\"\n @paste=\"onPaste\"\n >\n\n <span\n v-if=\"hasSuffix\"\n ref=\"suffixEl\"\n data-gr-input-tag-suffix\n class=\"inline-flex shrink-0 items-center justify-center text-[var(--gr-muted-fg)]\"\n :style=\"suffixStyle\"\n >\n <slot name=\"suffix\" />\n </span>\n\n <span\n v-if=\"isBusy\"\n data-gr-input-tag-spinner\n :class=\"spinnerClass\"\n aria-hidden=\"true\"\n >\n <GrIcon size=\"sm\">\n <IconLoader class=\"animate-spin\" />\n </GrIcon>\n </span>\n\n <button\n v-if=\"showClearAll\"\n type=\"button\"\n data-gr-input-tag-clear\n :class=\"clearButtonClass\"\n :aria-label=\"resolvedClearAllLabel\"\n @mousedown.prevent.stop\n @click.stop=\"clearAll\"\n >\n <GrIcon size=\"sm\">\n <IconClose />\n </GrIcon>\n </button>\n </div>\n</template>\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  import { n as grInputTagSafelist, t as grInputTagConfig } from "../../chunks/config-BqT6PMzO.js";
2
- import { t as GrInputTag_default } from "./chunks/GrInputTag-3Au5HWZL.js";
2
+ import { t as GrInputTag_default } from "./chunks/GrInputTag-BKzm1PKw.js";
3
3
  export { GrInputTag_default as GrInputTag, GrInputTag_default as default, grInputTagConfig, grInputTagSafelist };
@@ -223,14 +223,38 @@ var GrJsonViewer_default = /* @__PURE__ */ defineComponent({
223
223
  *
224
224
  * Контролируемого `expandedKeys` у `GrTree` нет, но `defaultExpandedKeys` —
225
225
  * не «начальное значение»: дерево следит за пропом и заменяет набор целиком.
226
- * Этого хватает и глубине по умолчанию, и кнопкам «раскрыть/свернуть всё»;
227
- * ручное раскрытие пользователя при этом сбрасывается от этих кнопок ровно
228
- * того и ждут.
226
+ * Значит набор обязан быть зеркалом того, что открыто, а не только того, что
227
+ * открыли кнопки: разойдись они первое же обновление данных вернуло бы дерево
228
+ * к памяти просмотрщика, то есть закрыло бы раскрытое руками.
229
229
  */
230
230
  const expandedPaths = ref([]);
231
- watch([nodes, resolvedDepth], ([list, depth]) => {
232
- expandedPaths.value = pathsToDepth(list, depth);
231
+ /** Глубина по умолчанию раскладывается один раз — на первом дереве с ветками. */
232
+ let depthApplied = false;
233
+ /**
234
+ * Обновление данных раскрытие не сбрасывает.
235
+ *
236
+ * Ответ сервиса опрашивают раз в N секунд, и объект каждый раз новый; сброс на
237
+ * каждую смену складывал бы дерево под руками. Из набора выпадают только
238
+ * исчезнувшие ветки, а глубина применяется заново лишь тогда, когда изменилась
239
+ * она сама.
240
+ */
241
+ watch([nodes, resolvedDepth], ([list, depth], previous) => {
242
+ const branches = branchPaths(list);
243
+ if (previous !== void 0 && previous[1] !== depth || !depthApplied && branches.length > 0) {
244
+ depthApplied = branches.length > 0;
245
+ expandedPaths.value = pathsToDepth(list, depth);
246
+ return;
247
+ }
248
+ const alive = new Set(branches);
249
+ expandedPaths.value = expandedPaths.value.filter((path) => alive.has(path));
233
250
  }, { immediate: true });
251
+ /** Дерево держит раскрытие у себя и сообщает о нём событиями — слушаем оба. */
252
+ function rememberExpanded(node) {
253
+ if (!expandedPaths.value.includes(node.path)) expandedPaths.value = [...expandedPaths.value, node.path];
254
+ }
255
+ function forgetExpanded(node) {
256
+ expandedPaths.value = expandedPaths.value.filter((path) => path !== node.path);
257
+ }
234
258
  function expandAll() {
235
259
  expandedPaths.value = branchPaths(nodes.value);
236
260
  }
@@ -356,7 +380,9 @@ var GrJsonViewer_default = /* @__PURE__ */ defineComponent({
356
380
  virtual: __props.virtual,
357
381
  "max-height": __props.maxHeight,
358
382
  size: unref(resolvedSize),
359
- "aria-label": __props.ariaLabel
383
+ "aria-label": __props.ariaLabel,
384
+ onNodeExpand: rememberExpanded,
385
+ onNodeCollapse: forgetExpanded
360
386
  }, {
361
387
  default: withCtx(({ data }) => [createElementVNode("span", {
362
388
  "data-gr-json-viewer-row": "",
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GrJsonViewer-BLCEWlwg.js","names":[],"sources":["../../../../src/components/GrJsonViewer/jsonToNodes.ts","../../../../src/components/GrJsonViewer/GrJsonViewer.vue","../../../../src/components/GrJsonViewer/GrJsonViewer.vue"],"sourcesContent":["/**\n * Разбор произвольного значения в узлы дерева.\n *\n * Компонент показывает данные из БД и от чужих сервисов, поэтому обход обязан\n * пережить всё: цикл, `BigInt`, функцию в поле, массив на десять тысяч записей,\n * строку в миллион символов. Уронить страницу он права не имеет.\n */\n\n/** Что за значение в узле. Решает и цвет, и то, раскрывается ли узел. */\nexport type GrJsonNodeKind\n = | 'object'\n | 'array'\n | 'string'\n | 'number'\n | 'boolean'\n | 'null'\n /** Значения, которых в JSON не бывает: функция, символ, `undefined`. */\n | 'unsupported'\n /** Хвост обрезанного массива — «ещё N элементов». Своего значения не имеет. */\n | 'truncation'\n\nexport type GrJsonNode = {\n /** Читаемый адрес узла (`$.items[3].name`). Он же ключ для `GrTree`. */\n path: string\n /** Имя ключа или индекс. Показывается слева от двоеточия. */\n label: string\n kind: GrJsonNodeKind\n /** Что видно в строке. У ветки — счётчик, у листа — значение, возможно обрезанное. */\n preview: string\n /** Полное значение. Копирование берёт его, а не `preview`. */\n value: unknown\n /** Показ обрезан: копирование всё равно отдаёт целое. */\n truncated: boolean\n isLeaf: boolean\n children?: GrJsonNode[]\n}\n\nexport type GrJsonToNodesOptions = {\n /** Подпись корня. */\n rootLabel?: string\n /** Длина строки, после которой значение обрезается в показе. */\n maxStringLength?: number\n /** Сколько элементов массива разбирать до заглушки «ещё N». */\n maxArrayItems?: number\n}\n\n/** Маркер ветки, которая уже встречалась выше по этой же цепочке предков. */\nexport const CIRCULAR_MARKER = '[Circular]'\n\nconst DEFAULT_ROOT_LABEL = '$'\nconst DEFAULT_MAX_STRING_LENGTH = 200\nconst DEFAULT_MAX_ARRAY_ITEMS = 100\n\n/** Ключ, который можно дописать через точку, не сломав адрес. */\nconst PLAIN_KEY = /^[A-Za-z_$][\\w$]*$/\n\nfunction appendKey(parentPath: string, key: string): string {\n return PLAIN_KEY.test(key) ? `${parentPath}.${key}` : `${parentPath}[${JSON.stringify(key)}]`\n}\n\n/**\n * Счётчик у ветки вместо её содержимого: свёрнутый узел обязан сказать, стоит\n * ли его раскрывать. Слово не склоняется — компонент не знает языка потребителя,\n * а число само по себе читается в любом.\n */\nfunction branchPreview(kind: 'object' | 'array', count: number): string {\n return kind === 'array' ? `[${count}]` : `{${count}}`\n}\n\nfunction scalarPreview(value: unknown, maxStringLength: number): { preview: string, truncated: boolean } {\n if (typeof value === 'string') {\n if (value.length <= maxStringLength)\n return { preview: JSON.stringify(value), truncated: false }\n\n // Кавычка открывается и не закрывается намеренно: закрытая читалась бы как\n // конец строки, а строка продолжается — её просто не показывают целиком.\n return { preview: `${JSON.stringify(value.slice(0, maxStringLength))}…`, truncated: true }\n }\n\n // `BigInt` представления в JSON не имеет, и `JSON.stringify` на нём бросает.\n if (typeof value === 'bigint')\n return { preview: `${value}n`, truncated: false }\n\n if (typeof value === 'function' || typeof value === 'symbol' || value === undefined)\n return { preview: String(value), truncated: false }\n\n return { preview: JSON.stringify(value) ?? String(value), truncated: false }\n}\n\nfunction kindOf(value: unknown): GrJsonNodeKind {\n if (value === null)\n return 'null'\n if (Array.isArray(value))\n return 'array'\n\n switch (typeof value) {\n case 'string': return 'string'\n case 'number': return 'number'\n case 'bigint': return 'number'\n case 'boolean': return 'boolean'\n case 'object': return 'object'\n // `undefined`, функция и символ представления в JSON не имеют — узел\n // остаётся, чтобы ключ не исчез из показа молча.\n case 'undefined':\n case 'function':\n case 'symbol': return 'unsupported'\n }\n\n // Недостижимо: ветки выше покрывают `typeof` целиком. Оставлено ради вывода\n // типов — без него TS не считает функцию всегда возвращающей.\n return 'unsupported'\n}\n\ntype BuildContext = {\n maxStringLength: number\n maxArrayItems: number\n /**\n * Цепочка предков текущего узла, а не все встреченные объекты.\n *\n * Разница принципиальна и отличает обход дерева от `replacer` у\n * `JSON.stringify`: тому стек недоступен, поэтому он метит `[Circular]` любую\n * повторную ссылку — в том числе объект, честно положенный в данные дважды.\n * Здесь стек есть, и маркер достаётся только настоящему циклу.\n */\n ancestors: Set<object>\n}\n\nfunction buildNode(label: string, path: string, value: unknown, context: BuildContext): GrJsonNode {\n const kind = kindOf(value)\n\n if (kind !== 'object' && kind !== 'array') {\n const { preview, truncated } = scalarPreview(value, context.maxStringLength)\n\n return { path, label, kind, preview, value, truncated, isLeaf: true }\n }\n\n const container = value as object\n\n if (context.ancestors.has(container)) {\n return { path, label, kind, preview: CIRCULAR_MARKER, value, truncated: false, isLeaf: true }\n }\n\n const entries = Array.isArray(value)\n ? value.map((item, index) => [String(index), item] as const)\n : Object.entries(container)\n\n // Пустой объект и пустой массив — листья: раскрывать нечего, а ветка без\n // детей рисует стрелку, которая ничего не делает.\n if (entries.length === 0) {\n return {\n path,\n label,\n kind,\n preview: Array.isArray(value) ? '[]' : '{}',\n value,\n truncated: false,\n isLeaf: true,\n }\n }\n\n context.ancestors.add(container)\n\n const limit = Math.max(0, context.maxArrayItems)\n const isOverLimit = Array.isArray(value) && entries.length > limit\n const visible = isOverLimit ? entries.slice(0, limit) : entries\n\n const children: GrJsonNode[] = visible.map(([key, item]) => buildNode(\n key,\n Array.isArray(value) ? `${path}[${key}]` : appendKey(path, key),\n item,\n context,\n ))\n\n if (isOverLimit) {\n const rest = entries.length - limit\n\n children.push({\n path: `${path}[…]`,\n label: '…',\n kind: 'truncation',\n preview: String(rest),\n value: (value as unknown[]).slice(limit),\n truncated: true,\n isLeaf: true,\n })\n }\n\n context.ancestors.delete(container)\n\n return {\n path,\n label,\n kind,\n preview: branchPreview(kind, entries.length),\n value,\n truncated: isOverLimit,\n isLeaf: false,\n children,\n }\n}\n\n/**\n * Значение → узлы дерева. Корень всегда один: даже скаляр на входе — это узел,\n * иначе дерево пришлось бы то показывать, то нет.\n */\nexport function jsonToNodes(value: unknown, options: GrJsonToNodesOptions = {}): GrJsonNode[] {\n const context: BuildContext = {\n maxStringLength: options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH,\n maxArrayItems: options.maxArrayItems ?? DEFAULT_MAX_ARRAY_ITEMS,\n ancestors: new Set(),\n }\n\n return [buildNode(options.rootLabel ?? DEFAULT_ROOT_LABEL, DEFAULT_ROOT_LABEL, value, context)]\n}\n\n/** Пути всех узлов до заданной глубины — ими задаётся начальная свёртка. */\nexport function pathsToDepth(nodes: GrJsonNode[], depth: number, level = 0): string[] {\n if (level >= depth)\n return []\n\n return nodes.flatMap(node => (\n node.children ? [node.path, ...pathsToDepth(node.children, depth, level + 1)] : []\n ))\n}\n\n/** Пути всех веток — для «раскрыть всё». */\nexport function branchPaths(nodes: GrJsonNode[]): string[] {\n return nodes.flatMap(node => (node.children ? [node.path, ...branchPaths(node.children)] : []))\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrTree from '../GrTree/GrTree.vue'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\n\nimport IconCheck from '~icons/lucide/check'\nimport IconCopy from '~icons/lucide/copy'\n\nimport { branchPaths, jsonToNodes, pathsToDepth, type GrJsonNode } from './jsonToNodes'\nimport {\n jsonValueClass,\n jsonViewerContentClass,\n jsonViewerCopyClass,\n jsonViewerGroupClass,\n jsonViewerKeyClass,\n jsonViewerMutedClass,\n jsonViewerPunctuationClass,\n jsonViewerRootClass,\n jsonViewerRowClass,\n jsonViewerSearchClass,\n jsonViewerToolbarClass,\n jsonViewerValueClass,\n} from './grJsonViewerStyles'\n\nimport type { GrComponentSize } from '../shared/sizes'\n\nexport type { GrJsonNode, GrJsonNodeKind } from './jsonToNodes'\n\n/**\n * JSON со свёрткой по узлам, поиском по ключу и значению и копированием узла.\n *\n * Дерево, клавиатуру и виртуализацию несёт `GrTree`; здесь — разбор `unknown` в\n * узлы и разметка строки. Плоский текст целиком, который копируют в тикет, —\n * `GrCodeBlock`.\n */\nexport interface GrJsonViewerProps {\n /** Показываемое значение. `unknown`, потому что приходит из БД или чужого сервиса. */\n value: unknown\n /** Подпись корня. По умолчанию `$` — так адресуют путь JSONPath и dev-tools. */\n rootLabel?: string\n /** Сколько уровней раскрыто изначально. */\n defaultExpandDepth?: number\n /**\n * Длина строкового значения, после которой показ обрезается.\n *\n * Не косметика: запрос к модели с картинкой в base64 — это один лист на\n * сотни тысяч символов, и ни свёртка, ни виртуализация по строкам его не\n * берут. Копирование при этом отдаёт значение целиком.\n */\n maxStringLength?: number\n /** Сколько элементов массива разбирать до заглушки «ещё N». */\n maxArrayItems?: number\n /** Поле поиска над деревом. Выключено — поиск остаётся на `filter()` из `defineExpose`. */\n searchable?: boolean\n /** Кнопка копирования узла в строке. */\n copyable?: boolean\n /** Оставлять в DOM только окно вокруг вьюпорта. Требует `maxHeight`. */\n virtual?: boolean\n /** Высота области просмотра: число — пиксели, строка — как есть. */\n maxHeight?: string | number\n /** Имя области для скринридера. Безымянное дерево объявляется просто «дерево». */\n ariaLabel?: string\n size?: GrComponentSize\n}\n\nexport interface GrJsonViewerEmits {\n /** Узел скопирован: путь для тикета и полное значение. */\n (e: 'copy', payload: { path: string, value: unknown }): void\n}\n\nconst props = withDefaults(defineProps<GrJsonViewerProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n rootLabel: undefined,\n defaultExpandDepth: undefined,\n maxStringLength: undefined,\n maxArrayItems: undefined,\n searchable: undefined,\n copyable: undefined,\n virtual: false,\n maxHeight: undefined,\n ariaLabel: undefined,\n size: undefined,\n})\n\nconst emit = defineEmits<GrJsonViewerEmits>()\n\ndefineSlots<{\n /** Значение листа: ссылка, дата, денежная сумма — оформление знает приложение. */\n leaf?: (props: { node: GrJsonNode }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\nconst { announce } = useAnnouncer()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrJsonViewer' })\nconst resolvedDepth = useGrComponentProp('GrJsonViewer', 'defaultExpandDepth', () => props.defaultExpandDepth, 1)\nconst resolvedMaxString = useGrComponentProp('GrJsonViewer', 'maxStringLength', () => props.maxStringLength, 200)\nconst resolvedMaxArray = useGrComponentProp('GrJsonViewer', 'maxArrayItems', () => props.maxArrayItems, 100)\nconst resolvedSearchable = useGrComponentProp('GrJsonViewer', 'searchable', () => props.searchable, true)\nconst resolvedCopyable = useGrComponentProp('GrJsonViewer', 'copyable', () => props.copyable, true)\n\nconst nodes = computed(() => jsonToNodes(props.value, {\n rootLabel: props.rootLabel,\n maxStringLength: resolvedMaxString.value,\n maxArrayItems: resolvedMaxArray.value,\n}))\n\n/**\n * Раскрытие ведётся подстановкой набора путей.\n *\n * Контролируемого `expandedKeys` у `GrTree` нет, но `defaultExpandedKeys` —\n * не «начальное значение»: дерево следит за пропом и заменяет набор целиком.\n * Значит набор обязан быть зеркалом того, что открыто, а не только того, что\n * открыли кнопки: разойдись они — первое же обновление данных вернуло бы дерево\n * к памяти просмотрщика, то есть закрыло бы раскрытое руками.\n */\nconst expandedPaths = ref<string[]>([])\n\n/** Глубина по умолчанию раскладывается один раз — на первом дереве с ветками. */\nlet depthApplied = false\n\n/**\n * Обновление данных раскрытие не сбрасывает.\n *\n * Ответ сервиса опрашивают раз в N секунд, и объект каждый раз новый; сброс на\n * каждую смену складывал бы дерево под руками. Из набора выпадают только\n * исчезнувшие ветки, а глубина применяется заново лишь тогда, когда изменилась\n * она сама.\n */\nwatch(\n [nodes, resolvedDepth],\n ([list, depth], previous) => {\n const branches = branchPaths(list)\n const depthChanged = previous !== undefined && previous[1] !== depth\n\n if (depthChanged || (!depthApplied && branches.length > 0)) {\n depthApplied = branches.length > 0\n expandedPaths.value = pathsToDepth(list, depth)\n return\n }\n\n const alive = new Set(branches)\n expandedPaths.value = expandedPaths.value.filter(path => alive.has(path))\n },\n { immediate: true },\n)\n\n/** Дерево держит раскрытие у себя и сообщает о нём событиями — слушаем оба. */\nfunction rememberExpanded(node: GrJsonNode): void {\n if (!expandedPaths.value.includes(node.path))\n expandedPaths.value = [...expandedPaths.value, node.path]\n}\n\nfunction forgetExpanded(node: GrJsonNode): void {\n expandedPaths.value = expandedPaths.value.filter(path => path !== node.path)\n}\n\nfunction expandAll(): void {\n expandedPaths.value = branchPaths(nodes.value)\n}\n\nfunction collapseAll(): void {\n expandedPaths.value = []\n}\n\nconst query = ref('')\nconst treeRef = ref<{ filter: (value: string) => void } | null>(null)\n\nwatch(query, (value) => {\n treeRef.value?.filter(value)\n})\n\nfunction filter(value: string): void {\n query.value = value\n}\n\n/** Поиск идёт и по ключу, и по значению: в чужом ответе ищут то одно, то другое. */\nfunction matchKeyOrValue(value: string, data: GrJsonNode): boolean {\n if (!value)\n return true\n\n const needle = value.toLowerCase()\n\n return data.label.toLowerCase().includes(needle) || data.preview.toLowerCase().includes(needle)\n}\n\n/**\n * Наличие буфера уточняется после монтирования: `navigator` в теле `setup` либо\n * роняет серверный рендер, либо расходится с ним. До этого момента кнопки нет —\n * ровно то же, что отдаёт сервер, поэтому гидрация совпадает.\n */\nconst canCopy = ref(false)\n\nonMounted(() => {\n canCopy.value = typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function'\n\n if (props.virtual && props.maxHeight === undefined && __GR_DEV__) {\n console.warn('[GrJsonViewer] `virtual` без `maxHeight` не включает виртуализацию: окно считается от высоты области.')\n }\n})\n\nconst showCopy = computed(() => resolvedCopyable.value && canCopy.value)\n\nconst copiedPath = ref<string | null>(null)\nlet copiedTimer: number | null = null\n\nfunction resetCopiedTimer(): void {\n if (copiedTimer !== null) {\n clearTimeout(copiedTimer)\n copiedTimer = null\n }\n}\n\nonBeforeUnmount(resetCopiedTimer)\n\n/**\n * В буфер уходит **полное** значение узла, а не то, что видно: обрезка\n * принадлежит показу, и вставить её обратно нельзя.\n */\nasync function copyNode(node: GrJsonNode): Promise<void> {\n const text = typeof node.value === 'string' ? node.value : JSON.stringify(node.value, null, 2) ?? String(node.value)\n\n try {\n await navigator.clipboard.writeText(text)\n }\n catch {\n announce(t('gr.jsonViewer.copyFailed', 'Could not copy value'))\n return\n }\n\n copiedPath.value = node.path\n resetCopiedTimer()\n copiedTimer = window.setTimeout(() => {\n copiedPath.value = null\n copiedTimer = null\n }, 1200)\n\n announce(t('gr.jsonViewer.copied', 'Value copied'))\n emit('copy', { path: node.path, value: node.value })\n}\n\ndefineExpose({ filter, expandAll, collapseAll })\n</script>\n\n<template>\n <div data-gr-json-viewer :class=\"jsonViewerRootClass\">\n <div v-if=\"resolvedSearchable\" data-gr-json-viewer-toolbar :class=\"jsonViewerToolbarClass\">\n <GrInput\n v-model=\"query\"\n :class=\"jsonViewerSearchClass\"\n :size=\"resolvedSize\"\n clearable\n :placeholder=\"t('gr.jsonViewer.search', 'Search key or value')\"\n :aria-label=\"t('gr.jsonViewer.search', 'Search key or value')\"\n />\n\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"expandAll\">\n {{ t('gr.jsonViewer.expandAll', 'Expand all') }}\n </GrButton>\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"collapseAll\">\n {{ t('gr.jsonViewer.collapseAll', 'Collapse all') }}\n </GrButton>\n </div>\n\n <GrTree\n ref=\"treeRef\"\n :data=\"nodes\"\n node-key=\"path\"\n :props=\"{ children: 'children', label: 'label', isLeaf: 'isLeaf' }\"\n :default-expanded-keys=\"expandedPaths\"\n :filter-node-method=\"matchKeyOrValue\"\n :content-class=\"jsonViewerContentClass\"\n :virtual=\"virtual\"\n :max-height=\"maxHeight\"\n :size=\"resolvedSize\"\n :aria-label=\"ariaLabel\"\n @node-expand=\"rememberExpanded\"\n @node-collapse=\"forgetExpanded\"\n >\n <template #default=\"{ data }\">\n <span data-gr-json-viewer-row :class=\"[jsonViewerRowClass, jsonViewerGroupClass]\">\n <span data-gr-json-viewer-key :class=\"jsonViewerKeyClass\">{{ data.label }}</span>\n <span :class=\"jsonViewerPunctuationClass\">:</span>\n\n <span\n data-gr-json-viewer-value\n :class=\"[jsonViewerValueClass, jsonValueClass[(data as GrJsonNode).kind]]\"\n >\n <slot name=\"leaf\" :node=\"data as GrJsonNode\">{{ (data as GrJsonNode).preview }}</slot>\n </span>\n\n <span\n v-if=\"(data as GrJsonNode).kind === 'truncation'\"\n :class=\"jsonViewerMutedClass\"\n >{{ t('gr.jsonViewer.moreItems', 'more') }}</span>\n\n <GrButton\n v-if=\"showCopy && (data as GrJsonNode).kind !== 'truncation'\"\n data-gr-json-viewer-copy\n variant=\"ghost\"\n size=\"xs\"\n square\n :class=\"jsonViewerCopyClass\"\n :aria-label=\"t('gr.jsonViewer.copy', 'Copy value')\"\n :title=\"t('gr.jsonViewer.copy', 'Copy value')\"\n @click.stop=\"copyNode(data as GrJsonNode)\"\n >\n <component\n :is=\"copiedPath === (data as GrJsonNode).path ? IconCheck : IconCopy\"\n class=\"h-3.5 w-3.5\"\n aria-hidden=\"true\"\n />\n </GrButton>\n </span>\n </template>\n </GrTree>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrTree from '../GrTree/GrTree.vue'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\n\nimport IconCheck from '~icons/lucide/check'\nimport IconCopy from '~icons/lucide/copy'\n\nimport { branchPaths, jsonToNodes, pathsToDepth, type GrJsonNode } from './jsonToNodes'\nimport {\n jsonValueClass,\n jsonViewerContentClass,\n jsonViewerCopyClass,\n jsonViewerGroupClass,\n jsonViewerKeyClass,\n jsonViewerMutedClass,\n jsonViewerPunctuationClass,\n jsonViewerRootClass,\n jsonViewerRowClass,\n jsonViewerSearchClass,\n jsonViewerToolbarClass,\n jsonViewerValueClass,\n} from './grJsonViewerStyles'\n\nimport type { GrComponentSize } from '../shared/sizes'\n\nexport type { GrJsonNode, GrJsonNodeKind } from './jsonToNodes'\n\n/**\n * JSON со свёрткой по узлам, поиском по ключу и значению и копированием узла.\n *\n * Дерево, клавиатуру и виртуализацию несёт `GrTree`; здесь — разбор `unknown` в\n * узлы и разметка строки. Плоский текст целиком, который копируют в тикет, —\n * `GrCodeBlock`.\n */\nexport interface GrJsonViewerProps {\n /** Показываемое значение. `unknown`, потому что приходит из БД или чужого сервиса. */\n value: unknown\n /** Подпись корня. По умолчанию `$` — так адресуют путь JSONPath и dev-tools. */\n rootLabel?: string\n /** Сколько уровней раскрыто изначально. */\n defaultExpandDepth?: number\n /**\n * Длина строкового значения, после которой показ обрезается.\n *\n * Не косметика: запрос к модели с картинкой в base64 — это один лист на\n * сотни тысяч символов, и ни свёртка, ни виртуализация по строкам его не\n * берут. Копирование при этом отдаёт значение целиком.\n */\n maxStringLength?: number\n /** Сколько элементов массива разбирать до заглушки «ещё N». */\n maxArrayItems?: number\n /** Поле поиска над деревом. Выключено — поиск остаётся на `filter()` из `defineExpose`. */\n searchable?: boolean\n /** Кнопка копирования узла в строке. */\n copyable?: boolean\n /** Оставлять в DOM только окно вокруг вьюпорта. Требует `maxHeight`. */\n virtual?: boolean\n /** Высота области просмотра: число — пиксели, строка — как есть. */\n maxHeight?: string | number\n /** Имя области для скринридера. Безымянное дерево объявляется просто «дерево». */\n ariaLabel?: string\n size?: GrComponentSize\n}\n\nexport interface GrJsonViewerEmits {\n /** Узел скопирован: путь для тикета и полное значение. */\n (e: 'copy', payload: { path: string, value: unknown }): void\n}\n\nconst props = withDefaults(defineProps<GrJsonViewerProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n rootLabel: undefined,\n defaultExpandDepth: undefined,\n maxStringLength: undefined,\n maxArrayItems: undefined,\n searchable: undefined,\n copyable: undefined,\n virtual: false,\n maxHeight: undefined,\n ariaLabel: undefined,\n size: undefined,\n})\n\nconst emit = defineEmits<GrJsonViewerEmits>()\n\ndefineSlots<{\n /** Значение листа: ссылка, дата, денежная сумма — оформление знает приложение. */\n leaf?: (props: { node: GrJsonNode }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\nconst { announce } = useAnnouncer()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrJsonViewer' })\nconst resolvedDepth = useGrComponentProp('GrJsonViewer', 'defaultExpandDepth', () => props.defaultExpandDepth, 1)\nconst resolvedMaxString = useGrComponentProp('GrJsonViewer', 'maxStringLength', () => props.maxStringLength, 200)\nconst resolvedMaxArray = useGrComponentProp('GrJsonViewer', 'maxArrayItems', () => props.maxArrayItems, 100)\nconst resolvedSearchable = useGrComponentProp('GrJsonViewer', 'searchable', () => props.searchable, true)\nconst resolvedCopyable = useGrComponentProp('GrJsonViewer', 'copyable', () => props.copyable, true)\n\nconst nodes = computed(() => jsonToNodes(props.value, {\n rootLabel: props.rootLabel,\n maxStringLength: resolvedMaxString.value,\n maxArrayItems: resolvedMaxArray.value,\n}))\n\n/**\n * Раскрытие ведётся подстановкой набора путей.\n *\n * Контролируемого `expandedKeys` у `GrTree` нет, но `defaultExpandedKeys` —\n * не «начальное значение»: дерево следит за пропом и заменяет набор целиком.\n * Значит набор обязан быть зеркалом того, что открыто, а не только того, что\n * открыли кнопки: разойдись они — первое же обновление данных вернуло бы дерево\n * к памяти просмотрщика, то есть закрыло бы раскрытое руками.\n */\nconst expandedPaths = ref<string[]>([])\n\n/** Глубина по умолчанию раскладывается один раз — на первом дереве с ветками. */\nlet depthApplied = false\n\n/**\n * Обновление данных раскрытие не сбрасывает.\n *\n * Ответ сервиса опрашивают раз в N секунд, и объект каждый раз новый; сброс на\n * каждую смену складывал бы дерево под руками. Из набора выпадают только\n * исчезнувшие ветки, а глубина применяется заново лишь тогда, когда изменилась\n * она сама.\n */\nwatch(\n [nodes, resolvedDepth],\n ([list, depth], previous) => {\n const branches = branchPaths(list)\n const depthChanged = previous !== undefined && previous[1] !== depth\n\n if (depthChanged || (!depthApplied && branches.length > 0)) {\n depthApplied = branches.length > 0\n expandedPaths.value = pathsToDepth(list, depth)\n return\n }\n\n const alive = new Set(branches)\n expandedPaths.value = expandedPaths.value.filter(path => alive.has(path))\n },\n { immediate: true },\n)\n\n/** Дерево держит раскрытие у себя и сообщает о нём событиями — слушаем оба. */\nfunction rememberExpanded(node: GrJsonNode): void {\n if (!expandedPaths.value.includes(node.path))\n expandedPaths.value = [...expandedPaths.value, node.path]\n}\n\nfunction forgetExpanded(node: GrJsonNode): void {\n expandedPaths.value = expandedPaths.value.filter(path => path !== node.path)\n}\n\nfunction expandAll(): void {\n expandedPaths.value = branchPaths(nodes.value)\n}\n\nfunction collapseAll(): void {\n expandedPaths.value = []\n}\n\nconst query = ref('')\nconst treeRef = ref<{ filter: (value: string) => void } | null>(null)\n\nwatch(query, (value) => {\n treeRef.value?.filter(value)\n})\n\nfunction filter(value: string): void {\n query.value = value\n}\n\n/** Поиск идёт и по ключу, и по значению: в чужом ответе ищут то одно, то другое. */\nfunction matchKeyOrValue(value: string, data: GrJsonNode): boolean {\n if (!value)\n return true\n\n const needle = value.toLowerCase()\n\n return data.label.toLowerCase().includes(needle) || data.preview.toLowerCase().includes(needle)\n}\n\n/**\n * Наличие буфера уточняется после монтирования: `navigator` в теле `setup` либо\n * роняет серверный рендер, либо расходится с ним. До этого момента кнопки нет —\n * ровно то же, что отдаёт сервер, поэтому гидрация совпадает.\n */\nconst canCopy = ref(false)\n\nonMounted(() => {\n canCopy.value = typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function'\n\n if (props.virtual && props.maxHeight === undefined && __GR_DEV__) {\n console.warn('[GrJsonViewer] `virtual` без `maxHeight` не включает виртуализацию: окно считается от высоты области.')\n }\n})\n\nconst showCopy = computed(() => resolvedCopyable.value && canCopy.value)\n\nconst copiedPath = ref<string | null>(null)\nlet copiedTimer: number | null = null\n\nfunction resetCopiedTimer(): void {\n if (copiedTimer !== null) {\n clearTimeout(copiedTimer)\n copiedTimer = null\n }\n}\n\nonBeforeUnmount(resetCopiedTimer)\n\n/**\n * В буфер уходит **полное** значение узла, а не то, что видно: обрезка\n * принадлежит показу, и вставить её обратно нельзя.\n */\nasync function copyNode(node: GrJsonNode): Promise<void> {\n const text = typeof node.value === 'string' ? node.value : JSON.stringify(node.value, null, 2) ?? String(node.value)\n\n try {\n await navigator.clipboard.writeText(text)\n }\n catch {\n announce(t('gr.jsonViewer.copyFailed', 'Could not copy value'))\n return\n }\n\n copiedPath.value = node.path\n resetCopiedTimer()\n copiedTimer = window.setTimeout(() => {\n copiedPath.value = null\n copiedTimer = null\n }, 1200)\n\n announce(t('gr.jsonViewer.copied', 'Value copied'))\n emit('copy', { path: node.path, value: node.value })\n}\n\ndefineExpose({ filter, expandAll, collapseAll })\n</script>\n\n<template>\n <div data-gr-json-viewer :class=\"jsonViewerRootClass\">\n <div v-if=\"resolvedSearchable\" data-gr-json-viewer-toolbar :class=\"jsonViewerToolbarClass\">\n <GrInput\n v-model=\"query\"\n :class=\"jsonViewerSearchClass\"\n :size=\"resolvedSize\"\n clearable\n :placeholder=\"t('gr.jsonViewer.search', 'Search key or value')\"\n :aria-label=\"t('gr.jsonViewer.search', 'Search key or value')\"\n />\n\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"expandAll\">\n {{ t('gr.jsonViewer.expandAll', 'Expand all') }}\n </GrButton>\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"collapseAll\">\n {{ t('gr.jsonViewer.collapseAll', 'Collapse all') }}\n </GrButton>\n </div>\n\n <GrTree\n ref=\"treeRef\"\n :data=\"nodes\"\n node-key=\"path\"\n :props=\"{ children: 'children', label: 'label', isLeaf: 'isLeaf' }\"\n :default-expanded-keys=\"expandedPaths\"\n :filter-node-method=\"matchKeyOrValue\"\n :content-class=\"jsonViewerContentClass\"\n :virtual=\"virtual\"\n :max-height=\"maxHeight\"\n :size=\"resolvedSize\"\n :aria-label=\"ariaLabel\"\n @node-expand=\"rememberExpanded\"\n @node-collapse=\"forgetExpanded\"\n >\n <template #default=\"{ data }\">\n <span data-gr-json-viewer-row :class=\"[jsonViewerRowClass, jsonViewerGroupClass]\">\n <span data-gr-json-viewer-key :class=\"jsonViewerKeyClass\">{{ data.label }}</span>\n <span :class=\"jsonViewerPunctuationClass\">:</span>\n\n <span\n data-gr-json-viewer-value\n :class=\"[jsonViewerValueClass, jsonValueClass[(data as GrJsonNode).kind]]\"\n >\n <slot name=\"leaf\" :node=\"data as GrJsonNode\">{{ (data as GrJsonNode).preview }}</slot>\n </span>\n\n <span\n v-if=\"(data as GrJsonNode).kind === 'truncation'\"\n :class=\"jsonViewerMutedClass\"\n >{{ t('gr.jsonViewer.moreItems', 'more') }}</span>\n\n <GrButton\n v-if=\"showCopy && (data as GrJsonNode).kind !== 'truncation'\"\n data-gr-json-viewer-copy\n variant=\"ghost\"\n size=\"xs\"\n square\n :class=\"jsonViewerCopyClass\"\n :aria-label=\"t('gr.jsonViewer.copy', 'Copy value')\"\n :title=\"t('gr.jsonViewer.copy', 'Copy value')\"\n @click.stop=\"copyNode(data as GrJsonNode)\"\n >\n <component\n :is=\"copiedPath === (data as GrJsonNode).path ? IconCheck : IconCopy\"\n class=\"h-3.5 w-3.5\"\n aria-hidden=\"true\"\n />\n </GrButton>\n </span>\n </template>\n </GrTree>\n </div>\n</template>\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { t as GrJsonViewer_default } from "./chunks/GrJsonViewer-CDGOX3C0.js";
1
+ import { t as GrJsonViewer_default } from "./chunks/GrJsonViewer-BLCEWlwg.js";
2
2
  import { n as grJsonViewerSafelist, t as grJsonViewerConfig } from "../../chunks/config-DpAYidyY.js";
3
3
  export { GrJsonViewer_default as GrJsonViewer, GrJsonViewer_default as default, grJsonViewerConfig, grJsonViewerSafelist };
@@ -1,7 +1,7 @@
1
1
  import { a as useGrComponentSize } from "../../../chunks/context-CPMRfwVr.js";
2
2
  import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
3
3
  import { a as shiftSatisfied, n as isAppleDevice, r as isComposingEvent, t as eventMatchesKey } from "../../../chunks/keyboard-DE2yhz_b.js";
4
- import { a as grKbdKeyClass, d as sequenceSeparatorClass, i as grKbdInnerKeyClass, r as grKbdComboClass, u as separatorClass } from "../../../chunks/grKbdStyles-DENu8pUK.js";
4
+ import { a as grKbdKeyClass, d as sequenceSeparatorClass, i as grKbdInnerKeyClass, r as grKbdComboClass, u as separatorClass } from "../../../chunks/grKbdStyles-ByMsX-lQ.js";
5
5
  import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, defineComponent, normalizeClass, onMounted, openBlock, ref, renderList, renderSlot, toDisplayString, unref } from "vue";
6
6
  //#region src/components/shared/hotkeyTokens.ts
7
7
  function symbol(label, name) {