@feugene/granularity 0.46.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/chunks/{x-circle-BCBURwyX.js → check-circle-Bnrb-CGJ.js} +8 -32
- package/dist/chunks/check-circle-Bnrb-CGJ.js.map +1 -0
- package/dist/chunks/{config-BqT6PMzO.js → config-B42iJ48O.js} +5 -1
- package/dist/chunks/{config-BqT6PMzO.js.map → config-B42iJ48O.js.map} +1 -1
- package/dist/chunks/{config-BGCyyXiD.js → config-BAse0laT.js} +5 -1
- package/dist/chunks/{config-BGCyyXiD.js.map → config-BAse0laT.js.map} +1 -1
- package/dist/chunks/{config-MmPym-Z7.js → config-BLP94dPY.js} +5 -1
- package/dist/chunks/{config-MmPym-Z7.js.map → config-BLP94dPY.js.map} +1 -1
- package/dist/chunks/{config-CjKpUcmy.js → config-Bdf1bt-m.js} +25 -16
- package/dist/chunks/config-Bdf1bt-m.js.map +1 -0
- package/dist/chunks/{config-DeyT9dJl.js → config-Bs1HwUcx.js} +47 -4
- package/dist/chunks/{config-DeyT9dJl.js.map → config-Bs1HwUcx.js.map} +1 -1
- package/dist/chunks/{config-CIkdsWUk.js → config-CIUU9ZpY.js} +4 -1
- package/dist/chunks/config-CIUU9ZpY.js.map +1 -0
- package/dist/chunks/{config-B3J0alyY.js → config-CzeZg2LO.js} +49 -4
- package/dist/chunks/config-CzeZg2LO.js.map +1 -0
- package/dist/chunks/{config-DCq4JySZ.js → config-D3HOuHDe.js} +1 -1
- package/dist/chunks/{config-DCq4JySZ.js.map → config-D3HOuHDe.js.map} +1 -1
- package/dist/chunks/{config-BhfNgyWL.js → config-DENqOoh0.js} +5 -1
- package/dist/chunks/config-DENqOoh0.js.map +1 -0
- package/dist/chunks/controlState-DoImKciE.js +35 -0
- package/dist/chunks/controlState-DoImKciE.js.map +1 -0
- package/dist/chunks/{en-DiHzFact.js → en-Bxyn5lW7.js} +12 -2
- package/dist/chunks/en-Bxyn5lW7.js.map +1 -0
- package/dist/chunks/{es-CeKkMxZ_.js → es-Bj_vc18Z.js} +3 -3
- package/dist/chunks/{es-CeKkMxZ_.js.map → es-Bj_vc18Z.js.map} +1 -1
- package/dist/chunks/{es-1ZL6NtkB.js → es-Dtl-a7ui.js} +12 -2
- package/dist/chunks/es-Dtl-a7ui.js.map +1 -0
- package/dist/chunks/{generated-CQ4h6Ieq.js → generated-hELWB0gZ.js} +98 -0
- package/dist/chunks/{generated-CQ4h6Ieq.js.map → generated-hELWB0gZ.js.map} +1 -1
- package/dist/chunks/{grTableStyles-uuMPSOBR.js → grTableStyles-DkQPMfx9.js} +16 -3
- package/dist/chunks/grTableStyles-DkQPMfx9.js.map +1 -0
- package/dist/chunks/{granular-provider-CsN_R9dB.js → granular-provider-BOQp7N5o.js} +9 -9
- package/dist/chunks/{granular-provider-CsN_R9dB.js.map → granular-provider-BOQp7N5o.js.map} +1 -1
- package/dist/chunks/{ru-Dr2RYn0P.js → ru-hrB4I1u_.js} +12 -2
- package/dist/chunks/ru-hrB4I1u_.js.map +1 -0
- package/dist/chunks/x-circle-BigSZy8m.js +26 -0
- package/dist/chunks/x-circle-BigSZy8m.js.map +1 -0
- package/dist/components/GrAlert/chunks/{GrAlert-D72y1IEh.js → GrAlert-CfEZghSY.js} +2 -1
- package/dist/components/GrAlert/chunks/{GrAlert-D72y1IEh.js.map → GrAlert-CfEZghSY.js.map} +1 -1
- package/dist/components/GrAlert/index.js +1 -1
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-D396_JrW.js → GrAutocomplete-C-uovdv8.js} +320 -224
- package/dist/components/GrAutocomplete/chunks/GrAutocomplete-C-uovdv8.js.map +1 -0
- package/dist/components/GrAutocomplete/index.js +1 -1
- package/dist/components/GrAutocomplete/styles.css +1 -1
- package/dist/components/GrAvatar/chunks/{GrAvatar-dWEXiCJG.js → GrAvatar-Do5-C409.js} +12 -1
- package/dist/components/GrAvatar/chunks/GrAvatar-Do5-C409.js.map +1 -0
- package/dist/components/GrAvatar/index.js +2 -2
- package/dist/components/GrAvatar/themes/light.css +35 -0
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-9mTR-YZ_.js → GrColorPicker-BRM_4lyI.js} +1 -1
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-9mTR-YZ_.js.map → GrColorPicker-BRM_4lyI.js.map} +1 -1
- package/dist/components/GrColorPicker/index.js +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-Ha4s3DMT.js.map → GrCommandPalette-BJHmkDQv.js.map} +1 -1
- package/dist/components/GrCommandPalette/index.js +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-e15nFI8M.js → GrConfirmDialog-BzUQwbec.js} +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-e15nFI8M.js.map → GrConfirmDialog-BzUQwbec.js.map} +1 -1
- package/dist/components/GrConfirmDialog/index.js +1 -1
- package/dist/components/GrDataTable/chunks/GrDataTable-DQxYYgQk.js +1519 -0
- package/dist/components/GrDataTable/chunks/GrDataTable-DQxYYgQk.js.map +1 -0
- package/dist/components/GrDataTable/index.js +2 -2
- package/dist/components/GrDataTable/themes/light.css +10 -0
- package/dist/components/GrDialogService/chunks/{GrDialogService-BmXiYmUp.js → GrDialogService-Dtq0ZCN5.js} +3 -3
- package/dist/components/GrDialogService/chunks/{GrDialogService-BmXiYmUp.js.map → GrDialogService-Dtq0ZCN5.js.map} +1 -1
- package/dist/components/GrDialogService/index.js +1 -1
- package/dist/components/GrFormField/chunks/GrFormField-DO61vxpS.js.map +1 -1
- package/dist/components/GrInput/chunks/{GrInput-TFeBm3gQ.js → GrInput-DV4gMWPR.js} +40 -11
- package/dist/components/GrInput/chunks/GrInput-DV4gMWPR.js.map +1 -0
- package/dist/components/GrInput/index.js +2 -2
- package/dist/components/GrInputTag/chunks/{GrInputTag-Ci8AO1ze.js → GrInputTag-CO0YU-ok.js} +30 -6
- package/dist/components/GrInputTag/chunks/GrInputTag-CO0YU-ok.js.map +1 -0
- package/dist/components/GrInputTag/index.js +2 -2
- package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-C6q8dEEZ.js → GrJsonViewer-D7CyWzhT.js} +1 -1
- package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-C6q8dEEZ.js.map → GrJsonViewer-D7CyWzhT.js.map} +1 -1
- package/dist/components/GrJsonViewer/index.js +1 -1
- package/dist/components/GrNumberInput/chunks/{GrNumberInput-CL5QhOmx.js → GrNumberInput-BI6-vkcs.js} +48 -16
- package/dist/components/GrNumberInput/chunks/GrNumberInput-BI6-vkcs.js.map +1 -0
- package/dist/components/GrNumberInput/index.js +2 -2
- package/dist/components/GrPagination/chunks/{GrPagination-DhbNZHEF.js → GrPagination-BbaF5eXk.js} +10 -7
- package/dist/components/GrPagination/chunks/GrPagination-BbaF5eXk.js.map +1 -0
- package/dist/components/GrPagination/index.js +2 -2
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-8RxQsOQ_.js → GrPromptDialog-BEL-QgwE.js} +3 -3
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-8RxQsOQ_.js.map → GrPromptDialog-BEL-QgwE.js.map} +1 -1
- package/dist/components/GrPromptDialog/index.js +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-NEEJZ8zi.js → GrResponseErrorBanner-Bdd5c07x.js} +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-NEEJZ8zi.js.map → GrResponseErrorBanner-Bdd5c07x.js.map} +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-ZvBvEQDZ.js → GrResponseErrorBanner-DySe1ym4.js} +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-ZvBvEQDZ.js.map → GrResponseErrorBanner-DySe1ym4.js.map} +1 -1
- package/dist/components/GrResponseErrorBanner/index.js +2 -2
- package/dist/components/GrSelect/chunks/{GrSelect-Do1iAyp1.js → GrSelect-0iI4wdR_.js} +3 -3
- package/dist/components/GrSelect/chunks/{GrSelect-Do1iAyp1.js.map → GrSelect-0iI4wdR_.js.map} +1 -1
- package/dist/components/GrSelect/index.js +1 -1
- package/dist/components/GrTable/chunks/{GrTable-iwzHvlMC.js → GrTable-CKJ-HrsN.js} +1 -1
- package/dist/components/GrTable/chunks/{GrTable-iwzHvlMC.js.map → GrTable-CKJ-HrsN.js.map} +1 -1
- package/dist/components/GrTable/index.js +2 -2
- package/dist/components/GrTextarea/chunks/{GrTextarea-BII3w61K.js → GrTextarea-Bfgc0DD2.js} +73 -32
- package/dist/components/GrTextarea/chunks/GrTextarea-Bfgc0DD2.js.map +1 -0
- package/dist/components/GrTextarea/index.js +2 -2
- package/dist/components/GrToaster/chunks/{GrToaster-D-sV-9of.js → GrToaster-CZixNk0l.js} +2 -1
- package/dist/components/GrToaster/chunks/{GrToaster-D-sV-9of.js.map → GrToaster-CZixNk0l.js.map} +1 -1
- package/dist/components/GrToaster/index.js +1 -1
- package/dist/components/GrTransfer/chunks/{GrTransfer-8FA78ny6.js → GrTransfer-DdfBUf35.js} +2 -2
- package/dist/components/GrTransfer/chunks/{GrTransfer-8FA78ny6.js.map → GrTransfer-DdfBUf35.js.map} +1 -1
- package/dist/components/GrTransfer/index.js +1 -1
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-DBu5nMGA.js → GrTreeSelect-B6KoDYiZ.js} +43 -18
- package/dist/components/GrTreeSelect/chunks/GrTreeSelect-B6KoDYiZ.js.map +1 -0
- package/dist/components/GrTreeSelect/index.js +2 -2
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/i18n/all.js +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/index.js +32 -32
- package/dist/theme.js +1 -1
- package/dist/tokens.js +1 -1
- package/dist/types/components/GrAutocomplete/composables/useAutocompleteNavigation.d.ts +39 -0
- package/dist/types/components/GrAutocomplete/composables/useAutocompletePanel.d.ts +36 -0
- package/dist/types/components/GrAutocomplete/composables/useAutocompleteValues.d.ts +28 -0
- package/dist/types/components/GrAutocomplete/composables/useAutocompleteVirtual.d.ts +33 -0
- package/dist/types/components/GrAutocomplete/composables/useRemoteOptions.d.ts +33 -0
- package/dist/types/components/GrAvatar/GrAvatar.vue.d.ts +11 -0
- package/dist/types/components/GrAvatar/defaults.d.ts +1 -0
- package/dist/types/components/GrAvatar/grAvatarStyles.d.ts +26 -1
- package/dist/types/components/GrDataTable/GrDataTable.vue.d.ts +60 -1
- package/dist/types/components/GrDataTable/composables/useDataTableColumnOrder.d.ts +45 -0
- package/dist/types/components/GrDataTable/composables/useDataTableColumnWidths.d.ts +38 -0
- package/dist/types/components/GrDataTable/composables/useDataTableExpansion.d.ts +46 -0
- package/dist/types/components/GrDataTable/composables/useDataTableLayout.d.ts +29 -0
- package/dist/types/components/GrDataTable/composables/useDataTableRowKeys.d.ts +16 -0
- package/dist/types/components/GrDataTable/composables/useDataTableSelection.d.ts +32 -0
- package/dist/types/components/GrDataTable/composables/useDataTableSort.d.ts +37 -0
- package/dist/types/components/GrDataTable/grDataTableStyles.d.ts +23 -0
- package/dist/types/components/GrFormField/GrFormField.vue.d.ts +7 -1
- package/dist/types/components/GrPagination/GrPagination.vue.d.ts +17 -2
- package/dist/types/components/GrPagination/grPaginationStyles.d.ts +15 -6
- package/dist/types/components/GrPagination/index.d.ts +1 -1
- package/dist/types/components/GrTable/grTableStyles.d.ts +16 -3
- package/dist/types/components/shared/controlState.d.ts +36 -0
- package/dist/types/i18n/messages/en.d.ts +20 -0
- package/dist/types/i18n/messages/es.d.ts +20 -0
- package/dist/types/i18n/messages/ru.d.ts +20 -0
- package/dist/web-types.json +79 -3
- package/package.json +1 -1
- package/dist/chunks/config-B3J0alyY.js.map +0 -1
- package/dist/chunks/config-BhfNgyWL.js.map +0 -1
- package/dist/chunks/config-CIkdsWUk.js.map +0 -1
- package/dist/chunks/config-CjKpUcmy.js.map +0 -1
- package/dist/chunks/en-DiHzFact.js.map +0 -1
- package/dist/chunks/es-1ZL6NtkB.js.map +0 -1
- package/dist/chunks/grTableStyles-uuMPSOBR.js.map +0 -1
- package/dist/chunks/ru-Dr2RYn0P.js.map +0 -1
- package/dist/chunks/x-circle-BCBURwyX.js.map +0 -1
- package/dist/components/GrAutocomplete/chunks/GrAutocomplete-D396_JrW.js.map +0 -1
- package/dist/components/GrAvatar/chunks/GrAvatar-dWEXiCJG.js.map +0 -1
- package/dist/components/GrDataTable/chunks/GrDataTable-xNSmO_I-.js +0 -1134
- package/dist/components/GrDataTable/chunks/GrDataTable-xNSmO_I-.js.map +0 -1
- package/dist/components/GrInput/chunks/GrInput-TFeBm3gQ.js.map +0 -1
- package/dist/components/GrInputTag/chunks/GrInputTag-Ci8AO1ze.js.map +0 -1
- package/dist/components/GrNumberInput/chunks/GrNumberInput-CL5QhOmx.js.map +0 -1
- package/dist/components/GrPagination/chunks/GrPagination-DhbNZHEF.js.map +0 -1
- package/dist/components/GrTextarea/chunks/GrTextarea-BII3w61K.js.map +0 -1
- package/dist/components/GrTreeSelect/chunks/GrTreeSelect-DBu5nMGA.js.map +0 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-Ha4s3DMT.js → GrCommandPalette-BJHmkDQv.js} +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrJsonViewer-C6q8dEEZ.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
|
+
{"version":3,"file":"GrJsonViewer-D7CyWzhT.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-
|
|
1
|
+
import { t as GrJsonViewer_default } from "./chunks/GrJsonViewer-D7CyWzhT.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 };
|
package/dist/components/GrNumberInput/chunks/{GrNumberInput-CL5QhOmx.js → GrNumberInput-BI6-vkcs.js}
RENAMED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as alert_triangle_default, t as check_circle_default } from "../../../chunks/check-circle-Bnrb-CGJ.js";
|
|
1
2
|
import { t as x_default } from "../../../chunks/x-2KXnqxiD.js";
|
|
2
3
|
import { a as useGrComponentSize, i as useGrComponentProp } from "../../../chunks/context-CPMRfwVr.js";
|
|
3
4
|
import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
|
|
@@ -8,9 +9,10 @@ import { n as addLen, t as useControlAddons } from "../../../chunks/useControlAd
|
|
|
8
9
|
import { t as chevron_down_default } from "../../../chunks/chevron-down-CQS2OG_s.js";
|
|
9
10
|
import { t as chevron_left_default } from "../../../chunks/chevron-left-D8AUBAM7.js";
|
|
10
11
|
import { t as chevron_right_default } from "../../../chunks/chevron-right-C6Z01h0X.js";
|
|
12
|
+
import { a as controlStateTextKey, i as controlStateIconColors, n as controlStateFallbackText, r as controlStateIconClass, t as controlSignalState } from "../../../chunks/controlState-DoImKciE.js";
|
|
11
13
|
import { t as bigStep } from "../../../chunks/numericStep-Bq2lj4ew.js";
|
|
12
|
-
import { a as grNumberInputShellClass, i as grNumberInputInputClass, o as stepperCompactClass, r as clearButtonClass, s as stepperWideClass } from "../../../chunks/config-
|
|
13
|
-
import { computed, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, markRaw, mergeProps, normalizeClass, normalizeStyle, onBeforeUnmount, openBlock, ref, renderSlot, unref, watchEffect, withCtx, withModifiers } from "vue";
|
|
14
|
+
import { a as grNumberInputShellClass, i as grNumberInputInputClass, o as stepperCompactClass, r as clearButtonClass, s as stepperWideClass } from "../../../chunks/config-BLP94dPY.js";
|
|
15
|
+
import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, markRaw, mergeProps, normalizeClass, normalizeStyle, onBeforeUnmount, openBlock, ref, renderSlot, resolveDynamicComponent, toDisplayString, unref, useId, watchEffect, withCtx, withModifiers } from "vue";
|
|
14
16
|
//#region src/components/GrNumberInput/numberInputMath.ts
|
|
15
17
|
/**
|
|
16
18
|
* Экспоненциальная запись разбирается отдельно: `String(1e-7)` — это `"1e-7"`,
|
|
@@ -79,12 +81,13 @@ var _hoisted_1 = [
|
|
|
79
81
|
"aria-label",
|
|
80
82
|
"readonly"
|
|
81
83
|
];
|
|
82
|
-
var _hoisted_2 = ["
|
|
83
|
-
var _hoisted_3 =
|
|
84
|
-
var _hoisted_4 =
|
|
84
|
+
var _hoisted_2 = ["id"];
|
|
85
|
+
var _hoisted_3 = ["aria-label"];
|
|
86
|
+
var _hoisted_4 = { class: "flex flex-col justify-center gap-1" };
|
|
85
87
|
var _hoisted_5 = ["disabled", "aria-label"];
|
|
86
88
|
var _hoisted_6 = ["disabled", "aria-label"];
|
|
87
89
|
var _hoisted_7 = ["disabled", "aria-label"];
|
|
90
|
+
var _hoisted_8 = ["disabled", "aria-label"];
|
|
88
91
|
var REPEAT_DELAY_MS = 400;
|
|
89
92
|
var REPEAT_INTERVAL_MS = 60;
|
|
90
93
|
//#endregion
|
|
@@ -181,7 +184,6 @@ var GrNumberInput_default = /* @__PURE__ */ defineComponent({
|
|
|
181
184
|
const props = __props;
|
|
182
185
|
const resolvedSize = useGrComponentSize(() => props.size, { component: "GrNumberInput" });
|
|
183
186
|
const resolvedId = computed(() => props.id ?? field?.id.value);
|
|
184
|
-
const describedBy = computed(() => field?.describedById.value);
|
|
185
187
|
const { disabled: isDisabled, invalid: isInvalid, required: isRequired, readonly: isReadonly } = useGrFormControl(() => props);
|
|
186
188
|
const emit = __emit;
|
|
187
189
|
const inputEl = ref(null);
|
|
@@ -481,6 +483,23 @@ var GrNumberInput_default = /* @__PURE__ */ defineComponent({
|
|
|
481
483
|
const resolvedClearable = useGrComponentProp("GrNumberInput", "clearable", () => props.clearable, false);
|
|
482
484
|
const resolvedClearLabel = computed(() => props.clearLabel ?? t("gr.input.clear", "Clear"));
|
|
483
485
|
const clearVisible = computed(() => resolvedClearable.value && !isDisabled.value && !isReadonly.value && props.modelValue !== null);
|
|
486
|
+
/**
|
|
487
|
+
* Небуквенный признак состояния: иконка для глаз, скрытая подпись для
|
|
488
|
+
* скринридера (`shared/controlState`). Встаёт левее крестика по той же
|
|
489
|
+
* арифметике — правая зона тут занята суффиксом и кнопками ±, и место
|
|
490
|
+
* приходится отсчитывать, а не полагаться на поток.
|
|
491
|
+
*/
|
|
492
|
+
const signalState = computed(() => controlSignalState(props.state, isInvalid.value));
|
|
493
|
+
const stateIcon = computed(() => signalState.value === "success" ? check_circle_default : alert_triangle_default);
|
|
494
|
+
const stateIconClass = computed(() => signalState.value ? `${controlStateIconClass} ${controlStateIconColors[signalState.value]}` : "");
|
|
495
|
+
const stateIconStyle = computed(() => {
|
|
496
|
+
const controls = rightControlsCount.value > 0 ? px(addonPx.value * rightControlsCount.value) : "0px";
|
|
497
|
+
const clear = clearVisible.value ? "26px" : "0px";
|
|
498
|
+
return { right: addLen(addLen(addLen(suffixLen.value, controls), clear), "6px") };
|
|
499
|
+
});
|
|
500
|
+
const stateText = computed(() => signalState.value ? t(controlStateTextKey[signalState.value], controlStateFallbackText[signalState.value]) : "");
|
|
501
|
+
const stateTextId = useId();
|
|
502
|
+
const describedBy = computed(() => [field?.describedById.value, signalState.value ? stateTextId : void 0].filter(Boolean).join(" ") || void 0);
|
|
484
503
|
function clear() {
|
|
485
504
|
draft.value = null;
|
|
486
505
|
emit("update:modelValue", null);
|
|
@@ -547,8 +566,21 @@ var GrNumberInput_default = /* @__PURE__ */ defineComponent({
|
|
|
547
566
|
style: normalizeStyle(unref(suffixStyle)),
|
|
548
567
|
"aria-hidden": "true"
|
|
549
568
|
}, [renderSlot(_ctx.$slots, "suffix")], 6)) : createCommentVNode("", true),
|
|
550
|
-
|
|
569
|
+
signalState.value ? (openBlock(), createElementBlock("span", {
|
|
551
570
|
key: 2,
|
|
571
|
+
"data-gr-number-input-state": "",
|
|
572
|
+
class: normalizeClass(["absolute top-1/2 -translate-y-1/2", stateIconClass.value]),
|
|
573
|
+
style: normalizeStyle(stateIconStyle.value),
|
|
574
|
+
"aria-hidden": "true"
|
|
575
|
+
}, [(openBlock(), createBlock(resolveDynamicComponent(stateIcon.value), { class: "h-4 w-4" }))], 6)) : createCommentVNode("", true),
|
|
576
|
+
signalState.value ? (openBlock(), createElementBlock("span", {
|
|
577
|
+
key: 3,
|
|
578
|
+
id: unref(stateTextId),
|
|
579
|
+
"data-gr-number-input-state-text": "",
|
|
580
|
+
class: "sr-only"
|
|
581
|
+
}, toDisplayString(stateText.value), 9, _hoisted_2)) : createCommentVNode("", true),
|
|
582
|
+
clearVisible.value ? (openBlock(), createElementBlock("button", {
|
|
583
|
+
key: 4,
|
|
552
584
|
type: "button",
|
|
553
585
|
"data-gr-number-input-clear": "",
|
|
554
586
|
class: normalizeClass(unref(clearButtonClass)),
|
|
@@ -559,14 +591,14 @@ var GrNumberInput_default = /* @__PURE__ */ defineComponent({
|
|
|
559
591
|
}, [createVNode(GrIcon_default, { size: 14 }, {
|
|
560
592
|
default: withCtx(() => [createVNode(unref(x_default))]),
|
|
561
593
|
_: 1
|
|
562
|
-
})], 46,
|
|
594
|
+
})], 46, _hoisted_3)) : createCommentVNode("", true),
|
|
563
595
|
hasVerticalControls.value ? (openBlock(), createElementBlock("div", {
|
|
564
|
-
key:
|
|
596
|
+
key: 5,
|
|
565
597
|
"data-testid": "number-input-controls-vertical",
|
|
566
598
|
"data-gr-number-input-controls": "vertical",
|
|
567
599
|
class: "absolute inset-y-0 flex items-center justify-center border-l border-[var(--gr-brd)]",
|
|
568
600
|
style: normalizeStyle(verticalControlsStyle.value)
|
|
569
|
-
}, [createElementVNode("div",
|
|
601
|
+
}, [createElementVNode("div", _hoisted_4, [createElementVNode("button", {
|
|
570
602
|
type: "button",
|
|
571
603
|
"data-gr-number-input-increase": "",
|
|
572
604
|
class: normalizeClass(unref(stepperCompactClass)),
|
|
@@ -581,7 +613,7 @@ var GrNumberInput_default = /* @__PURE__ */ defineComponent({
|
|
|
581
613
|
}, [createVNode(GrIcon_default, { size: 12 }, {
|
|
582
614
|
default: withCtx(() => [createVNode(unref(chevron_up_default))]),
|
|
583
615
|
_: 1
|
|
584
|
-
})], 42,
|
|
616
|
+
})], 42, _hoisted_5), createElementVNode("button", {
|
|
585
617
|
type: "button",
|
|
586
618
|
"data-gr-number-input-decrease": "",
|
|
587
619
|
class: normalizeClass(unref(stepperCompactClass)),
|
|
@@ -596,9 +628,9 @@ var GrNumberInput_default = /* @__PURE__ */ defineComponent({
|
|
|
596
628
|
}, [createVNode(GrIcon_default, { size: 12 }, {
|
|
597
629
|
default: withCtx(() => [createVNode(unref(chevron_down_default))]),
|
|
598
630
|
_: 1
|
|
599
|
-
})], 42,
|
|
631
|
+
})], 42, _hoisted_6)])], 4)) : createCommentVNode("", true),
|
|
600
632
|
hasHorizontalControls.value ? (openBlock(), createElementBlock("div", {
|
|
601
|
-
key:
|
|
633
|
+
key: 6,
|
|
602
634
|
"data-testid": "number-input-controls-horizontal-left",
|
|
603
635
|
"data-gr-number-input-controls": "horizontal-left",
|
|
604
636
|
class: "absolute inset-y-0 flex items-stretch justify-center border-r border-[var(--gr-brd)]",
|
|
@@ -618,9 +650,9 @@ var GrNumberInput_default = /* @__PURE__ */ defineComponent({
|
|
|
618
650
|
}, [createVNode(GrIcon_default, { size: 12 }, {
|
|
619
651
|
default: withCtx(() => [createVNode(unref(chevron_left_default))]),
|
|
620
652
|
_: 1
|
|
621
|
-
})], 42,
|
|
653
|
+
})], 42, _hoisted_7)], 4)) : createCommentVNode("", true),
|
|
622
654
|
hasHorizontalControls.value ? (openBlock(), createElementBlock("div", {
|
|
623
|
-
key:
|
|
655
|
+
key: 7,
|
|
624
656
|
"data-testid": "number-input-controls-horizontal-right",
|
|
625
657
|
"data-gr-number-input-controls": "horizontal-right",
|
|
626
658
|
class: "absolute inset-y-0 flex items-stretch justify-center border-l border-[var(--gr-brd)]",
|
|
@@ -640,7 +672,7 @@ var GrNumberInput_default = /* @__PURE__ */ defineComponent({
|
|
|
640
672
|
}, [createVNode(GrIcon_default, { size: 12 }, {
|
|
641
673
|
default: withCtx(() => [createVNode(unref(chevron_right_default))]),
|
|
642
674
|
_: 1
|
|
643
|
-
})], 42,
|
|
675
|
+
})], 42, _hoisted_8)], 4)) : createCommentVNode("", true)
|
|
644
676
|
], 2);
|
|
645
677
|
};
|
|
646
678
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GrNumberInput-BI6-vkcs.js","names":["$slots","$attrs"],"sources":["../../../../src/components/GrNumberInput/numberInputMath.ts","../../../../src/components/GrNumberInput/GrNumberInput.vue","../../../../src/components/GrNumberInput/GrNumberInput.vue"],"sourcesContent":["export { bigStep } from '../shared/numericStep'\n\n/**\n * Экспоненциальная запись разбирается отдельно: `String(1e-7)` — это `\"1e-7\"`,\n * и поиск точки насчитал бы ноль знаков, после чего округление съело бы всё\n * значение.\n */\nexport function decimalsOf(value: number): number {\n if (!Number.isFinite(value))\n return 0\n\n const text = String(Math.abs(value))\n\n const exponential = text.match(/^\\d+(?:\\.(\\d+))?e-(\\d+)$/)\n if (exponential) {\n const [, fraction = '', exponent] = exponential\n return fraction.length + Number(exponent)\n }\n\n const dot = text.indexOf('.')\n return dot === -1 ? 0 : text.length - dot - 1\n}\n\n/**\n * Двоичная дробь копит ошибку, поэтому сумма округляется. Разрядность — по\n * большему из операндов: шаг `0.5` от значения `1.25` обязан дать `1.75`.\n */\nexport function addStep(current: number, step: number): number {\n const decimals = Math.max(decimalsOf(current), decimalsOf(step))\n const sum = current + step\n\n // `toFixed` за пределами 100 знаков бросает.\n return decimals > 100 ? sum : Number(sum.toFixed(decimals))\n}\n","<script setup lang=\"ts\">\nimport type { InputHTMLAttributes } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { computed, onBeforeUnmount, ref, useId, watchEffect } from 'vue'\n\nimport {\n clearButtonClass,\n stepperCompactClass,\n stepperWideClass,\n grNumberInputInputClass,\n grNumberInputShellClass,\n type GrNumberInputControlsDirection,\n type GrNumberInputSize,\n type GrNumberInputState,\n type GrNumberInputTextAlign,\n} from './grNumberInputStyles'\nimport { addStep, bigStep } from './numberInputMath'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport IconChevronDown from '~icons/lucide/chevron-down'\nimport IconChevronLeft from '~icons/lucide/chevron-left'\nimport IconChevronRight from '~icons/lucide/chevron-right'\nimport IconChevronUp from '~icons/lucide/chevron-up'\nimport IconX from '~icons/lucide/x'\nimport IconCheckCircle from '~icons/lucide/check-circle'\nimport IconAlertTriangle from '~icons/lucide/alert-triangle'\nimport { addLen } from '../../composables/internal/useAddonMeasurement'\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport {\n controlSignalState,\n controlStateFallbackText,\n controlStateIconClass,\n controlStateIconColors,\n controlStateTextKey,\n} from '../shared/controlState'\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst ADDON_PX_BY_SIZE: Record<GrNumberInputSize, number> = {\n xs: 32,\n sm: 36,\n md: 40,\n lg: 48,\n}\n\nconst BASE_PADDING_X_LEN_BY_SIZE: Record<GrNumberInputSize, string> = {\n xs: '10px',\n sm: '12px',\n md: '12px',\n lg: '16px',\n}\n\nfunction px(n: number): string {\n return `${n}px`\n}\n\nexport interface GrNumberInputProps {\n /**\n * Значение поля. `null` — пусто.\n *\n * Незавершённый ввод («-», «1,») числом не является и в модель не попадает:\n * пока он набирается, поле держит его во внутреннем черновике, а модель\n * честно говорит «числа пока нет».\n */\n modelValue: number | null\n placeholder?: string\n autocomplete?: string\n inputmode?: InputHTMLAttributes['inputmode']\n disabled?: boolean\n /** Быстрый флаг невалидности; эквивалент `state='danger'` + `aria-invalid`. */\n invalid?: boolean\n /** Только для чтения: значение видно и уходит в форму, но не редактируется. */\n readonly?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n state?: GrNumberInputState\n name?: string\n id?: string\n size?: GrNumberInputSize\n\n textAlign?: GrNumberInputTextAlign\n\n decimalSeparator?: string\n step?: number\n min?: number\n max?: number\n precision?: number\n\n /** Кнопка очистки значения. */\n clearable?: boolean\n /** A11y-подпись кнопки очистки. */\n clearLabel?: string\n /**\n * BCP-47 локаль для отображения значения (группировка разрядов и разделители\n * через `Intl.NumberFormat`). Работает вместе с `useGrouping`.\n */\n locale?: string\n /**\n * Группировать разряды при отображении (когда поле не в фокусе). При фокусе\n * показывается «сырое» значение для редактирования. По умолчанию выключено.\n */\n useGrouping?: boolean\n\n /** Показывать кнопки +/-. */\n controls?: boolean\n controlsDirection?: GrNumberInputControlsDirection\n\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n /**\n * Фиксированная ширина у prefix/suffix: жёсткая ширина (из `*MaxWidth` →\n * `*MinWidth` → дефолт) + обрезка контента по краю (prefix — справа,\n * suffix — слева). По умолчанию аддоны растягиваются под контент.\n */\n prefixFixed?: boolean\n suffixFixed?: boolean\n\n /** i18n-friendly aria-label для кнопки \"увеличить\". */\n increaseLabel?: string\n /** i18n-friendly aria-label для кнопки \"уменьшить\". */\n decreaseLabel?: string\n}\n\nexport interface GrNumberInputEmits {\n (e: 'update:modelValue', value: number | null): void\n (e: 'change', value: number | null): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n /** Значение стёрто кнопкой очистки. */\n (e: 'clear'): void\n}\n\n// Контекст `GrFormField` — fallback для id/описания/невалидности, как в `GrInput`.\nconst field = useGrFormFieldContext()\n\nconst props = withDefaults(defineProps<GrNumberInputProps>(), {\n placeholder: undefined,\n autocomplete: undefined,\n inputmode: 'decimal',\n disabled: false,\n invalid: false,\n readonly: false,\n required: false,\n ariaLabel: undefined,\n state: 'default',\n name: undefined,\n id: undefined,\n size: undefined,\n\n textAlign: 'left',\n\n decimalSeparator: '.',\n step: 1,\n min: undefined,\n max: undefined,\n precision: undefined,\n locale: undefined,\n useGrouping: false,\n clearable: undefined,\n clearLabel: undefined,\n\n controls: false,\n controlsDirection: 'vertical',\n\n prefixMinWidth: undefined,\n prefixMaxWidth: undefined,\n suffixMinWidth: undefined,\n suffixMaxWidth: undefined,\n prefixFixed: false,\n suffixFixed: false,\n\n increaseLabel: undefined,\n decreaseLabel: undefined,\n})\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrNumberInput' })\n\nconst resolvedId = computed(() => props.id ?? field?.id.value)\n\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nconst emit = defineEmits<GrNumberInputEmits>()\ndefineSlots<{\n /** Аддон слева от поля: знак валюты, иконка. */\n prefix?: () => any\n /** Аддон справа от поля: единица измерения. */\n suffix?: () => any\n}>()\n\nconst inputEl = ref<HTMLInputElement | null>(null)\n\n// В фокусе поле показывает «сырое» значение: группировка мешала бы правке.\nconst isFocused = ref(false)\n\n/**\n * Строка ровно в том виде, в каком её набирают. Модель к этому моменту может\n * быть `null` («-», «1,» — не числа), и без черновика поле стирало бы под\n * пальцами то, что печатают. Коммит (`change`/`blur`) черновик снимает.\n */\nconst draft = ref<string | null>(null)\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\ndefineExpose({ focus, blur })\n\nconst { t, locale } = useGranularityTranslations()\n\n/**\n * Локаль форматирования: свой проп → локаль i18n-адаптера → окружение. Без\n * второго шага мультиязычное приложение передавало бы `locale` каждому полю.\n */\nconst resolvedLocale = computed(() => props.locale ?? locale.value)\nconst resolvedIncreaseLabel = computed(() => props.increaseLabel ?? t('gr.numberInput.increase', 'Increase'))\nconst resolvedDecreaseLabel = computed(() => props.decreaseLabel ?? t('gr.numberInput.decrease', 'Decrease'))\n\nconst hasHorizontalControls = computed(() => props.controls && props.controlsDirection === 'horizontal')\nconst hasVerticalControls = computed(() => props.controls && props.controlsDirection === 'vertical')\n\nconst addonPx = computed(() => ADDON_PX_BY_SIZE[resolvedSize.value])\nconst addonLen = computed(() => px(addonPx.value))\nconst basePaddingXLen = computed(() => BASE_PADDING_X_LEN_BY_SIZE[resolvedSize.value])\n\n/** Кнопки ± справа: вертикальный стек — одна колонка, горизонтальный — вторая. */\nconst rightControlsCount = computed(() =>\n (hasHorizontalControls.value ? 1 : 0) + (hasVerticalControls.value ? 1 : 0))\n\nconst {\n prefixEl,\n suffixEl,\n prefixLen,\n suffixLen,\n prefixStyle,\n suffixStyle,\n fieldPadding: inputStyle,\n} = useControlAddons(() => props, {\n defaultMinWidth: () => addonLen.value,\n paddingX: () => basePaddingXLen.value,\n leadingReserve: () => (hasHorizontalControls.value ? addonLen.value : '0px'),\n trailingReserve: () => (rightControlsCount.value > 0 ? px(addonPx.value * rightControlsCount.value) : '0px'),\n // Суффикс позиционируется абсолютно: без якоря он уехал бы от правого края.\n anchorSuffixRight: true,\n})\n\nfunction addonStyle(side: 'left' | 'right', offset: string): Record<string, string> {\n return {\n width: addonLen.value,\n [side]: offset,\n }\n}\n\nconst verticalControlsStyle = computed(() => addonStyle('right', suffixLen.value))\n\n/**\n * Крестик очистки живёт левее правой зоны (суффикс + кнопки ±) — иначе он лёг бы\n * на них. Ширины там же, где их считает поле, поэтому переиспользуем расчёт.\n */\nconst clearButtonStyle = computed(() => {\n const controls = rightControlsCount.value > 0 ? px(addonPx.value * rightControlsCount.value) : '0px'\n\n return { right: addLen(addLen(suffixLen.value, controls), '6px') }\n})\n\nconst horizontalLeftControlsStyle = computed(() => addonStyle('left', prefixLen.value))\nconst horizontalRightControlsStyle = computed(() => addonStyle('right', suffixLen.value))\n\nconst shellClassName = computed(() => {\n return grNumberInputShellClass({\n disabled: isDisabled.value,\n state: props.state,\n invalid: isInvalid.value,\n })\n})\n\nconst inputClassName = computed(() => {\n return grNumberInputInputClass({\n size: resolvedSize.value,\n textAlign: props.textAlign,\n })\n})\n\nfunction isDigit(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction sanitize(raw: string): string {\n const sep = props.decimalSeparator || '.'\n\n let out = ''\n let hasSepLocal = false\n\n for (let i = 0; i < raw.length; i++) {\n const ch = raw[i]\n\n // Ведущий минус: разрешаем один в начале, чтобы можно было ввести\n // отрицательное значение с клавиатуры (границы проверит `clamp` на change).\n if (ch === '-' && out.length === 0) {\n out += '-'\n continue\n }\n\n if (isDigit(ch)) {\n out += ch\n continue\n }\n\n if ((ch === sep || ch === '.' || ch === ',') && !hasSepLocal) {\n out += sep\n hasSepLocal = true\n }\n }\n\n return out\n}\n\nfunction toNumber(value: string): number | null {\n if (value.trim() === '')\n return null\n const sep = props.decimalSeparator || '.'\n const normalized = sep === '.' ? value : value.replace(sep, '.')\n const n = Number(normalized)\n if (!Number.isFinite(n))\n return null\n return n\n}\n\nfunction clamp(v: number): number {\n if (props.min !== undefined)\n v = Math.max(props.min, v)\n if (props.max !== undefined)\n v = Math.min(props.max, v)\n return v\n}\n\nfunction normalize(v: number): number {\n if (props.precision === undefined)\n return v\n const p = Math.max(0, props.precision)\n return Number(v.toFixed(p))\n}\n\nfunction format(n: number): string {\n const sep = props.decimalSeparator || '.'\n const p = props.precision\n\n // Без `precision` избегаем экспоненциальной записи (`String(1e21)` → `\"1e+21\"`),\n // форматируя через `Intl` без группировки разрядов.\n let s = p === undefined\n ? n.toLocaleString('en-US', { useGrouping: false, maximumFractionDigits: 20 })\n : n.toFixed(Math.max(0, p))\n if (sep !== '.')\n s = s.replace('.', sep)\n return s\n}\n\nfunction onInput(e: Event): void {\n const el = e.target as HTMLInputElement\n const raw = el.value\n const next = sanitize(raw)\n\n if (raw !== next) {\n // Сохраняем позицию каретки: считаем, сколько валидных символов было ДО\n // каретки, и ставим её после стольких же в очищенной строке — иначе при\n // редактировании середины числа каретка прыгала в конец.\n const caret = el.selectionStart ?? raw.length\n const keptBefore = sanitize(raw.slice(0, caret)).length\n el.value = next\n el.setSelectionRange(keptBefore, keptBefore)\n }\n\n draft.value = next\n\n const num = toNumber(next)\n if (num !== props.modelValue)\n emit('update:modelValue', num)\n}\n\n/**\n * Коммит набранного: границы и точность применяются здесь, а не на каждом\n * нажатии, — иначе `min=10` не дал бы набрать «1» как начало «15».\n */\nfunction commit(): void {\n // Без черновика коммитится сама модель: значение вне границ обязано\n // выправиться и тогда, когда его выставили снаружи, а не набрали руками.\n const num = draft.value !== null ? toNumber(draft.value) : props.modelValue\n const next = num === null ? null : clamp(normalize(num))\n\n draft.value = null\n\n if (next !== props.modelValue)\n emit('update:modelValue', next)\n emit('change', next)\n}\n\nfunction onChange(): void {\n // Клампинг границ (`min`/`max`) и нормализация — на `change`/`blur`, а не только\n // в кнопках: иначе ручной ввод «999» при `max=10` останется невалидным.\n commit()\n}\n\nfunction setValue(n: number): void {\n const next = clamp(normalize(n))\n // Шаг завершает набор: черновик снимается, показ считается от модели.\n draft.value = null\n emit('update:modelValue', next)\n emit('change', next)\n}\n\n/**\n * Шаг значения. Фокус не трогаем: кнопка ± обязана оставаться под фокусом,\n * иначе клавиатурный пользователь после первого Enter теряет её и повторно\n * нажать не может. Поле фокусирует тот, кто шагает от него самого.\n */\nfunction stepBy(dir: 1 | -1, step: number = props.step ?? 1): void {\n if (isDisabled.value || isReadonly.value)\n return\n\n const current = props.modelValue ?? 0\n setValue(addStep(current, step * dir))\n}\n\n// Интервал уже шагал: финальный `click` при отпускании — не намерение\n// пользователя сделать ещё один шаг, а хвост того же жеста.\nlet repeatFired = false\n\n/** Клик кнопки «±»: шаг, если это не хвост только что завершённого удержания. */\nfunction onStepClick(dir: 1 | -1, event: MouseEvent): void {\n // Enter/Space на кнопке дают click без указателя (`detail === 0`) — хвостом\n // жеста он быть не может. Иначе удержание, кончившееся без клика (отпускание\n // вне кнопки, `pointercancel` на таче), оставляло бы флаг взведённым, и\n // спинбаттон молча терял бы нажатие у клавиатуры и вспомогательных технологий.\n if (event.detail === 0) {\n repeatFired = false\n stepBy(dir)\n return\n }\n\n if (repeatFired) {\n repeatFired = false\n return\n }\n stepBy(dir)\n}\n\nfunction onKeydown(e: KeyboardEvent): void {\n if (isDisabled.value)\n return\n // Readonly-поле ведёт себя как текст: стрелки и Home/End отдаются нативной\n // каретке, значение не меняется.\n if (isReadonly.value)\n return\n\n switch (e.key) {\n case 'ArrowUp':\n e.preventDefault()\n stepBy(1)\n break\n case 'ArrowDown':\n e.preventDefault()\n stepBy(-1)\n break\n case 'PageUp':\n e.preventDefault()\n stepBy(1, bigStep(props.step ?? 1, props.min, props.max))\n break\n case 'PageDown':\n e.preventDefault()\n stepBy(-1, bigStep(props.step ?? 1, props.min, props.max))\n break\n case 'Home':\n if (props.min !== undefined) {\n e.preventDefault()\n setValue(props.min)\n }\n break\n case 'End':\n if (props.max !== undefined) {\n e.preventDefault()\n setValue(props.max)\n }\n break\n }\n}\n\n// `aria-valuenow` — числовое значение для скринридеров; отсутствует, если поле пусто.\nconst ariaValueNow = computed(() => props.modelValue ?? undefined)\n\n// На пределе кнопка ± гаснет: активная кнопка, у которой `clamp` съедает\n// результат, читается как сломанная.\nconst numericValue = computed(() => props.modelValue)\n\nconst canIncrease = computed(() => {\n if (props.max === undefined)\n return true\n const current = numericValue.value\n return current === null || normalize(current) < props.max\n})\n\nconst canDecrease = computed(() => {\n if (props.min === undefined)\n return true\n const current = numericValue.value\n return current === null || normalize(current) > props.min\n})\n\n// Локале-зависимое отображение: при фокусе показываем «сырое» значение для\n// редактирования, иначе — сгруппированное через `Intl.NumberFormat`.\nfunction formatGrouped(num: number): string {\n const sep = props.decimalSeparator || '.'\n // formatToParts позволяет оставить локале-зависимый групповой разделитель,\n // но принудительно подставить наш десятичный разделитель (`decimalSeparator`),\n // корректно работая для любой локали (в т.ч. de-DE, где группа — '.').\n return new Intl.NumberFormat(resolvedLocale.value, {\n useGrouping: true,\n minimumFractionDigits: props.precision ?? 0,\n maximumFractionDigits: props.precision ?? 20,\n })\n .formatToParts(num)\n .map(part => (part.type === 'decimal' ? sep : part.value))\n .join('')\n}\n\n/**\n * `aria-valuetext` нужен только при группировке: без неё `aria-valuenow` уже\n * несёт то же самое число, и дублировать его текстом незачем.\n */\nconst ariaValueText = computed(() => {\n if (!props.useGrouping)\n return undefined\n return props.modelValue === null ? undefined : formatGrouped(props.modelValue)\n})\n\n/**\n * Что показывает поле: набираемый черновик, пока он есть, иначе — модель.\n * Группировка только вне фокуса: при правке разделители разрядов мешают.\n */\nconst displayValue = computed(() => {\n if (draft.value !== null)\n return draft.value\n if (props.modelValue === null)\n return ''\n if (props.useGrouping && !isFocused.value)\n return formatGrouped(props.modelValue)\n return format(props.modelValue)\n})\n\nfunction onFocus(event: FocusEvent): void {\n isFocused.value = true\n emit('focus', event)\n}\n\nfunction onBlur(event: FocusEvent): void {\n isFocused.value = false\n // `change` до `blur` доходит не всегда (значение могло вернуться к исходному),\n // а черновик обязан сняться в любом случае — иначе поле застынет на нём.\n draft.value = null\n emit('blur', event)\n stopRepeat()\n}\n\n// ————— Удержание кнопки ±: шаг повторяется, пока кнопку держат.\nconst REPEAT_DELAY_MS = 400\nconst REPEAT_INTERVAL_MS = 60\n\nlet repeatDelay: ReturnType<typeof setTimeout> | null = null\nlet repeatTimer: ReturnType<typeof setInterval> | null = null\n\nfunction stopRepeat(): void {\n if (repeatDelay !== null) {\n clearTimeout(repeatDelay)\n repeatDelay = null\n }\n if (repeatTimer !== null) {\n clearInterval(repeatTimer)\n repeatTimer = null\n }\n}\n\nfunction canStep(dir: 1 | -1): boolean {\n return dir === 1 ? canIncrease.value : canDecrease.value\n}\n\n/**\n * `pointerdown`, а не `mousedown`: на тач-устройствах второго не бывает.\n * Повтор останавливается на границе — иначе таймер крутился бы вхолостую.\n */\nfunction startRepeat(dir: 1 | -1): void {\n if (isDisabled.value || isReadonly.value)\n return\n\n stopRepeat()\n repeatFired = false\n repeatDelay = setTimeout(() => {\n repeatTimer = setInterval(() => {\n if (!canStep(dir)) {\n stopRepeat()\n return\n }\n repeatFired = true\n stepBy(dir)\n }, REPEAT_INTERVAL_MS)\n }, REPEAT_DELAY_MS)\n}\n\nonBeforeUnmount(stopRepeat)\n\n// ————— Очистка значения.\nconst resolvedClearable = useGrComponentProp('GrNumberInput', 'clearable', () => props.clearable, false)\nconst resolvedClearLabel = computed(() => props.clearLabel ?? t('gr.input.clear', 'Clear'))\n\nconst clearVisible = computed(() =>\n resolvedClearable.value && !isDisabled.value && !isReadonly.value && props.modelValue !== null,\n)\n\n/**\n * Небуквенный признак состояния: иконка для глаз, скрытая подпись для\n * скринридера (`shared/controlState`). Встаёт левее крестика по той же\n * арифметике — правая зона тут занята суффиксом и кнопками ±, и место\n * приходится отсчитывать, а не полагаться на поток.\n */\nconst signalState = computed(() => controlSignalState(props.state, isInvalid.value))\nconst stateIcon = computed(() => (signalState.value === 'success' ? IconCheckCircle : IconAlertTriangle))\nconst stateIconClass = computed(() => (signalState.value\n ? `${controlStateIconClass} ${controlStateIconColors[signalState.value]}`\n : ''))\nconst stateIconStyle = computed(() => {\n const controls = rightControlsCount.value > 0 ? px(addonPx.value * rightControlsCount.value) : '0px'\n // Крестик 20px плюс зазор: без сдвига признак лёг бы под него.\n const clear = clearVisible.value ? '26px' : '0px'\n\n return { right: addLen(addLen(addLen(suffixLen.value, controls), clear), '6px') }\n})\n\nconst stateText = computed(() => (signalState.value\n ? t(controlStateTextKey[signalState.value], controlStateFallbackText[signalState.value])\n : ''))\n\nconst stateTextId = useId()\nconst describedBy = computed(() =>\n [field?.describedById.value, signalState.value ? stateTextId : undefined]\n .filter(Boolean)\n .join(' ') || undefined,\n)\n\nfunction clear(): void {\n draft.value = null\n emit('update:modelValue', null)\n emit('change', null)\n emit('clear')\n focus()\n}\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (props.modelValue === undefined) {\n console.warn(\n `[granularity] GrNumberInput: обязательный проп \\`modelValue\\` не передан — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <div\n data-gr-number-input\n class=\"relative w-full overflow-hidden rounded-[var(--gr-radius-control)] border bg-[var(--gr-bg)] transition-colors duration-[var(--gr-duration-fast)] focus-within:ring-2 focus-within:ring-[var(--gr-ring)]\"\n :class=\"shellClassName\"\n >\n <div\n v-if=\"$slots.prefix\"\n ref=\"prefixEl\"\n data-testid=\"number-input-prefix\"\n data-gr-number-input-prefix\n class=\"absolute inset-y-0 left-0 flex items-center justify-center border-r border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :style=\"prefixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"prefix\" />\n </div>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n v-bind=\"$attrs\"\n :name=\"name\"\n type=\"text\"\n :inputmode=\"inputmode\"\n :autocomplete=\"autocomplete\"\n :placeholder=\"placeholder\"\n :disabled=\"isDisabled\"\n :value=\"displayValue\"\n role=\"spinbutton\"\n :aria-valuenow=\"ariaValueNow\"\n :aria-valuemin=\"min\"\n :aria-valuemax=\"max\"\n :aria-valuetext=\"ariaValueText\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :readonly=\"isReadonly\"\n class=\"w-full bg-transparent text-[var(--gr-fg)] placeholder:text-[var(--gr-muted-fg)] focus:placeholder:text-transparent focus:outline-none disabled:cursor-not-allowed\"\n :class=\"inputClassName\"\n :style=\"inputStyle\"\n @input=\"onInput\"\n @change=\"onChange\"\n @keydown=\"onKeydown\"\n @focus=\"onFocus\"\n @blur=\"onBlur\"\n >\n\n <div\n v-if=\"$slots.suffix\"\n ref=\"suffixEl\"\n data-testid=\"number-input-suffix\"\n data-gr-number-input-suffix\n class=\"absolute inset-y-0 flex items-center justify-center border-l border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"suffixFixed ? '[direction:rtl]' : ''\"\n :style=\"suffixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"suffix\" />\n </div>\n\n <span\n v-if=\"signalState\"\n data-gr-number-input-state\n class=\"absolute top-1/2 -translate-y-1/2\"\n :class=\"stateIconClass\"\n :style=\"stateIconStyle\"\n aria-hidden=\"true\"\n >\n <component :is=\"stateIcon\" class=\"h-4 w-4\" />\n </span>\n\n <span v-if=\"signalState\" :id=\"stateTextId\" data-gr-number-input-state-text class=\"sr-only\">{{ stateText }}</span>\n\n <button\n v-if=\"clearVisible\"\n type=\"button\"\n data-gr-number-input-clear\n :class=\"clearButtonClass\"\n :style=\"clearButtonStyle\"\n :aria-label=\"resolvedClearLabel\"\n @mousedown.prevent\n @click=\"clear\"\n >\n <GrIcon :size=\"14\">\n <IconX />\n </GrIcon>\n </button>\n\n <div\n v-if=\"hasVerticalControls\"\n data-testid=\"number-input-controls-vertical\"\n data-gr-number-input-controls=\"vertical\"\n class=\"absolute inset-y-0 flex items-center justify-center border-l border-[var(--gr-brd)]\"\n :style=\"verticalControlsStyle\"\n >\n <div class=\"flex flex-col justify-center gap-1\">\n <button\n type=\"button\"\n data-gr-number-input-increase\n :class=\"stepperCompactClass\"\n :disabled=\"isDisabled || isReadonly || !canIncrease\"\n :aria-label=\"resolvedIncreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronUp />\n </GrIcon>\n </button>\n <button\n type=\"button\"\n data-gr-number-input-decrease\n :class=\"stepperCompactClass\"\n :disabled=\"isDisabled || isReadonly || !canDecrease\"\n :aria-label=\"resolvedDecreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(-1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(-1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronDown />\n </GrIcon>\n </button>\n </div>\n </div>\n\n <div\n v-if=\"hasHorizontalControls\"\n data-testid=\"number-input-controls-horizontal-left\"\n data-gr-number-input-controls=\"horizontal-left\"\n class=\"absolute inset-y-0 flex items-stretch justify-center border-r border-[var(--gr-brd)]\"\n :style=\"horizontalLeftControlsStyle\"\n >\n <button\n type=\"button\"\n data-gr-number-input-decrease\n :class=\"stepperWideClass\"\n :disabled=\"isDisabled || isReadonly || !canDecrease\"\n :aria-label=\"resolvedDecreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(-1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(-1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronLeft />\n </GrIcon>\n </button>\n </div>\n\n <div\n v-if=\"hasHorizontalControls\"\n data-testid=\"number-input-controls-horizontal-right\"\n data-gr-number-input-controls=\"horizontal-right\"\n class=\"absolute inset-y-0 flex items-stretch justify-center border-l border-[var(--gr-brd)]\"\n :style=\"horizontalRightControlsStyle\"\n >\n <button\n type=\"button\"\n data-gr-number-input-increase\n :class=\"stepperWideClass\"\n :disabled=\"isDisabled || isReadonly || !canIncrease\"\n :aria-label=\"resolvedIncreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronRight />\n </GrIcon>\n </button>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type { InputHTMLAttributes } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { computed, onBeforeUnmount, ref, useId, watchEffect } from 'vue'\n\nimport {\n clearButtonClass,\n stepperCompactClass,\n stepperWideClass,\n grNumberInputInputClass,\n grNumberInputShellClass,\n type GrNumberInputControlsDirection,\n type GrNumberInputSize,\n type GrNumberInputState,\n type GrNumberInputTextAlign,\n} from './grNumberInputStyles'\nimport { addStep, bigStep } from './numberInputMath'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport IconChevronDown from '~icons/lucide/chevron-down'\nimport IconChevronLeft from '~icons/lucide/chevron-left'\nimport IconChevronRight from '~icons/lucide/chevron-right'\nimport IconChevronUp from '~icons/lucide/chevron-up'\nimport IconX from '~icons/lucide/x'\nimport IconCheckCircle from '~icons/lucide/check-circle'\nimport IconAlertTriangle from '~icons/lucide/alert-triangle'\nimport { addLen } from '../../composables/internal/useAddonMeasurement'\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport {\n controlSignalState,\n controlStateFallbackText,\n controlStateIconClass,\n controlStateIconColors,\n controlStateTextKey,\n} from '../shared/controlState'\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst ADDON_PX_BY_SIZE: Record<GrNumberInputSize, number> = {\n xs: 32,\n sm: 36,\n md: 40,\n lg: 48,\n}\n\nconst BASE_PADDING_X_LEN_BY_SIZE: Record<GrNumberInputSize, string> = {\n xs: '10px',\n sm: '12px',\n md: '12px',\n lg: '16px',\n}\n\nfunction px(n: number): string {\n return `${n}px`\n}\n\nexport interface GrNumberInputProps {\n /**\n * Значение поля. `null` — пусто.\n *\n * Незавершённый ввод («-», «1,») числом не является и в модель не попадает:\n * пока он набирается, поле держит его во внутреннем черновике, а модель\n * честно говорит «числа пока нет».\n */\n modelValue: number | null\n placeholder?: string\n autocomplete?: string\n inputmode?: InputHTMLAttributes['inputmode']\n disabled?: boolean\n /** Быстрый флаг невалидности; эквивалент `state='danger'` + `aria-invalid`. */\n invalid?: boolean\n /** Только для чтения: значение видно и уходит в форму, но не редактируется. */\n readonly?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n state?: GrNumberInputState\n name?: string\n id?: string\n size?: GrNumberInputSize\n\n textAlign?: GrNumberInputTextAlign\n\n decimalSeparator?: string\n step?: number\n min?: number\n max?: number\n precision?: number\n\n /** Кнопка очистки значения. */\n clearable?: boolean\n /** A11y-подпись кнопки очистки. */\n clearLabel?: string\n /**\n * BCP-47 локаль для отображения значения (группировка разрядов и разделители\n * через `Intl.NumberFormat`). Работает вместе с `useGrouping`.\n */\n locale?: string\n /**\n * Группировать разряды при отображении (когда поле не в фокусе). При фокусе\n * показывается «сырое» значение для редактирования. По умолчанию выключено.\n */\n useGrouping?: boolean\n\n /** Показывать кнопки +/-. */\n controls?: boolean\n controlsDirection?: GrNumberInputControlsDirection\n\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n /**\n * Фиксированная ширина у prefix/suffix: жёсткая ширина (из `*MaxWidth` →\n * `*MinWidth` → дефолт) + обрезка контента по краю (prefix — справа,\n * suffix — слева). По умолчанию аддоны растягиваются под контент.\n */\n prefixFixed?: boolean\n suffixFixed?: boolean\n\n /** i18n-friendly aria-label для кнопки \"увеличить\". */\n increaseLabel?: string\n /** i18n-friendly aria-label для кнопки \"уменьшить\". */\n decreaseLabel?: string\n}\n\nexport interface GrNumberInputEmits {\n (e: 'update:modelValue', value: number | null): void\n (e: 'change', value: number | null): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n /** Значение стёрто кнопкой очистки. */\n (e: 'clear'): void\n}\n\n// Контекст `GrFormField` — fallback для id/описания/невалидности, как в `GrInput`.\nconst field = useGrFormFieldContext()\n\nconst props = withDefaults(defineProps<GrNumberInputProps>(), {\n placeholder: undefined,\n autocomplete: undefined,\n inputmode: 'decimal',\n disabled: false,\n invalid: false,\n readonly: false,\n required: false,\n ariaLabel: undefined,\n state: 'default',\n name: undefined,\n id: undefined,\n size: undefined,\n\n textAlign: 'left',\n\n decimalSeparator: '.',\n step: 1,\n min: undefined,\n max: undefined,\n precision: undefined,\n locale: undefined,\n useGrouping: false,\n clearable: undefined,\n clearLabel: undefined,\n\n controls: false,\n controlsDirection: 'vertical',\n\n prefixMinWidth: undefined,\n prefixMaxWidth: undefined,\n suffixMinWidth: undefined,\n suffixMaxWidth: undefined,\n prefixFixed: false,\n suffixFixed: false,\n\n increaseLabel: undefined,\n decreaseLabel: undefined,\n})\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrNumberInput' })\n\nconst resolvedId = computed(() => props.id ?? field?.id.value)\n\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nconst emit = defineEmits<GrNumberInputEmits>()\ndefineSlots<{\n /** Аддон слева от поля: знак валюты, иконка. */\n prefix?: () => any\n /** Аддон справа от поля: единица измерения. */\n suffix?: () => any\n}>()\n\nconst inputEl = ref<HTMLInputElement | null>(null)\n\n// В фокусе поле показывает «сырое» значение: группировка мешала бы правке.\nconst isFocused = ref(false)\n\n/**\n * Строка ровно в том виде, в каком её набирают. Модель к этому моменту может\n * быть `null` («-», «1,» — не числа), и без черновика поле стирало бы под\n * пальцами то, что печатают. Коммит (`change`/`blur`) черновик снимает.\n */\nconst draft = ref<string | null>(null)\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\ndefineExpose({ focus, blur })\n\nconst { t, locale } = useGranularityTranslations()\n\n/**\n * Локаль форматирования: свой проп → локаль i18n-адаптера → окружение. Без\n * второго шага мультиязычное приложение передавало бы `locale` каждому полю.\n */\nconst resolvedLocale = computed(() => props.locale ?? locale.value)\nconst resolvedIncreaseLabel = computed(() => props.increaseLabel ?? t('gr.numberInput.increase', 'Increase'))\nconst resolvedDecreaseLabel = computed(() => props.decreaseLabel ?? t('gr.numberInput.decrease', 'Decrease'))\n\nconst hasHorizontalControls = computed(() => props.controls && props.controlsDirection === 'horizontal')\nconst hasVerticalControls = computed(() => props.controls && props.controlsDirection === 'vertical')\n\nconst addonPx = computed(() => ADDON_PX_BY_SIZE[resolvedSize.value])\nconst addonLen = computed(() => px(addonPx.value))\nconst basePaddingXLen = computed(() => BASE_PADDING_X_LEN_BY_SIZE[resolvedSize.value])\n\n/** Кнопки ± справа: вертикальный стек — одна колонка, горизонтальный — вторая. */\nconst rightControlsCount = computed(() =>\n (hasHorizontalControls.value ? 1 : 0) + (hasVerticalControls.value ? 1 : 0))\n\nconst {\n prefixEl,\n suffixEl,\n prefixLen,\n suffixLen,\n prefixStyle,\n suffixStyle,\n fieldPadding: inputStyle,\n} = useControlAddons(() => props, {\n defaultMinWidth: () => addonLen.value,\n paddingX: () => basePaddingXLen.value,\n leadingReserve: () => (hasHorizontalControls.value ? addonLen.value : '0px'),\n trailingReserve: () => (rightControlsCount.value > 0 ? px(addonPx.value * rightControlsCount.value) : '0px'),\n // Суффикс позиционируется абсолютно: без якоря он уехал бы от правого края.\n anchorSuffixRight: true,\n})\n\nfunction addonStyle(side: 'left' | 'right', offset: string): Record<string, string> {\n return {\n width: addonLen.value,\n [side]: offset,\n }\n}\n\nconst verticalControlsStyle = computed(() => addonStyle('right', suffixLen.value))\n\n/**\n * Крестик очистки живёт левее правой зоны (суффикс + кнопки ±) — иначе он лёг бы\n * на них. Ширины там же, где их считает поле, поэтому переиспользуем расчёт.\n */\nconst clearButtonStyle = computed(() => {\n const controls = rightControlsCount.value > 0 ? px(addonPx.value * rightControlsCount.value) : '0px'\n\n return { right: addLen(addLen(suffixLen.value, controls), '6px') }\n})\n\nconst horizontalLeftControlsStyle = computed(() => addonStyle('left', prefixLen.value))\nconst horizontalRightControlsStyle = computed(() => addonStyle('right', suffixLen.value))\n\nconst shellClassName = computed(() => {\n return grNumberInputShellClass({\n disabled: isDisabled.value,\n state: props.state,\n invalid: isInvalid.value,\n })\n})\n\nconst inputClassName = computed(() => {\n return grNumberInputInputClass({\n size: resolvedSize.value,\n textAlign: props.textAlign,\n })\n})\n\nfunction isDigit(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction sanitize(raw: string): string {\n const sep = props.decimalSeparator || '.'\n\n let out = ''\n let hasSepLocal = false\n\n for (let i = 0; i < raw.length; i++) {\n const ch = raw[i]\n\n // Ведущий минус: разрешаем один в начале, чтобы можно было ввести\n // отрицательное значение с клавиатуры (границы проверит `clamp` на change).\n if (ch === '-' && out.length === 0) {\n out += '-'\n continue\n }\n\n if (isDigit(ch)) {\n out += ch\n continue\n }\n\n if ((ch === sep || ch === '.' || ch === ',') && !hasSepLocal) {\n out += sep\n hasSepLocal = true\n }\n }\n\n return out\n}\n\nfunction toNumber(value: string): number | null {\n if (value.trim() === '')\n return null\n const sep = props.decimalSeparator || '.'\n const normalized = sep === '.' ? value : value.replace(sep, '.')\n const n = Number(normalized)\n if (!Number.isFinite(n))\n return null\n return n\n}\n\nfunction clamp(v: number): number {\n if (props.min !== undefined)\n v = Math.max(props.min, v)\n if (props.max !== undefined)\n v = Math.min(props.max, v)\n return v\n}\n\nfunction normalize(v: number): number {\n if (props.precision === undefined)\n return v\n const p = Math.max(0, props.precision)\n return Number(v.toFixed(p))\n}\n\nfunction format(n: number): string {\n const sep = props.decimalSeparator || '.'\n const p = props.precision\n\n // Без `precision` избегаем экспоненциальной записи (`String(1e21)` → `\"1e+21\"`),\n // форматируя через `Intl` без группировки разрядов.\n let s = p === undefined\n ? n.toLocaleString('en-US', { useGrouping: false, maximumFractionDigits: 20 })\n : n.toFixed(Math.max(0, p))\n if (sep !== '.')\n s = s.replace('.', sep)\n return s\n}\n\nfunction onInput(e: Event): void {\n const el = e.target as HTMLInputElement\n const raw = el.value\n const next = sanitize(raw)\n\n if (raw !== next) {\n // Сохраняем позицию каретки: считаем, сколько валидных символов было ДО\n // каретки, и ставим её после стольких же в очищенной строке — иначе при\n // редактировании середины числа каретка прыгала в конец.\n const caret = el.selectionStart ?? raw.length\n const keptBefore = sanitize(raw.slice(0, caret)).length\n el.value = next\n el.setSelectionRange(keptBefore, keptBefore)\n }\n\n draft.value = next\n\n const num = toNumber(next)\n if (num !== props.modelValue)\n emit('update:modelValue', num)\n}\n\n/**\n * Коммит набранного: границы и точность применяются здесь, а не на каждом\n * нажатии, — иначе `min=10` не дал бы набрать «1» как начало «15».\n */\nfunction commit(): void {\n // Без черновика коммитится сама модель: значение вне границ обязано\n // выправиться и тогда, когда его выставили снаружи, а не набрали руками.\n const num = draft.value !== null ? toNumber(draft.value) : props.modelValue\n const next = num === null ? null : clamp(normalize(num))\n\n draft.value = null\n\n if (next !== props.modelValue)\n emit('update:modelValue', next)\n emit('change', next)\n}\n\nfunction onChange(): void {\n // Клампинг границ (`min`/`max`) и нормализация — на `change`/`blur`, а не только\n // в кнопках: иначе ручной ввод «999» при `max=10` останется невалидным.\n commit()\n}\n\nfunction setValue(n: number): void {\n const next = clamp(normalize(n))\n // Шаг завершает набор: черновик снимается, показ считается от модели.\n draft.value = null\n emit('update:modelValue', next)\n emit('change', next)\n}\n\n/**\n * Шаг значения. Фокус не трогаем: кнопка ± обязана оставаться под фокусом,\n * иначе клавиатурный пользователь после первого Enter теряет её и повторно\n * нажать не может. Поле фокусирует тот, кто шагает от него самого.\n */\nfunction stepBy(dir: 1 | -1, step: number = props.step ?? 1): void {\n if (isDisabled.value || isReadonly.value)\n return\n\n const current = props.modelValue ?? 0\n setValue(addStep(current, step * dir))\n}\n\n// Интервал уже шагал: финальный `click` при отпускании — не намерение\n// пользователя сделать ещё один шаг, а хвост того же жеста.\nlet repeatFired = false\n\n/** Клик кнопки «±»: шаг, если это не хвост только что завершённого удержания. */\nfunction onStepClick(dir: 1 | -1, event: MouseEvent): void {\n // Enter/Space на кнопке дают click без указателя (`detail === 0`) — хвостом\n // жеста он быть не может. Иначе удержание, кончившееся без клика (отпускание\n // вне кнопки, `pointercancel` на таче), оставляло бы флаг взведённым, и\n // спинбаттон молча терял бы нажатие у клавиатуры и вспомогательных технологий.\n if (event.detail === 0) {\n repeatFired = false\n stepBy(dir)\n return\n }\n\n if (repeatFired) {\n repeatFired = false\n return\n }\n stepBy(dir)\n}\n\nfunction onKeydown(e: KeyboardEvent): void {\n if (isDisabled.value)\n return\n // Readonly-поле ведёт себя как текст: стрелки и Home/End отдаются нативной\n // каретке, значение не меняется.\n if (isReadonly.value)\n return\n\n switch (e.key) {\n case 'ArrowUp':\n e.preventDefault()\n stepBy(1)\n break\n case 'ArrowDown':\n e.preventDefault()\n stepBy(-1)\n break\n case 'PageUp':\n e.preventDefault()\n stepBy(1, bigStep(props.step ?? 1, props.min, props.max))\n break\n case 'PageDown':\n e.preventDefault()\n stepBy(-1, bigStep(props.step ?? 1, props.min, props.max))\n break\n case 'Home':\n if (props.min !== undefined) {\n e.preventDefault()\n setValue(props.min)\n }\n break\n case 'End':\n if (props.max !== undefined) {\n e.preventDefault()\n setValue(props.max)\n }\n break\n }\n}\n\n// `aria-valuenow` — числовое значение для скринридеров; отсутствует, если поле пусто.\nconst ariaValueNow = computed(() => props.modelValue ?? undefined)\n\n// На пределе кнопка ± гаснет: активная кнопка, у которой `clamp` съедает\n// результат, читается как сломанная.\nconst numericValue = computed(() => props.modelValue)\n\nconst canIncrease = computed(() => {\n if (props.max === undefined)\n return true\n const current = numericValue.value\n return current === null || normalize(current) < props.max\n})\n\nconst canDecrease = computed(() => {\n if (props.min === undefined)\n return true\n const current = numericValue.value\n return current === null || normalize(current) > props.min\n})\n\n// Локале-зависимое отображение: при фокусе показываем «сырое» значение для\n// редактирования, иначе — сгруппированное через `Intl.NumberFormat`.\nfunction formatGrouped(num: number): string {\n const sep = props.decimalSeparator || '.'\n // formatToParts позволяет оставить локале-зависимый групповой разделитель,\n // но принудительно подставить наш десятичный разделитель (`decimalSeparator`),\n // корректно работая для любой локали (в т.ч. de-DE, где группа — '.').\n return new Intl.NumberFormat(resolvedLocale.value, {\n useGrouping: true,\n minimumFractionDigits: props.precision ?? 0,\n maximumFractionDigits: props.precision ?? 20,\n })\n .formatToParts(num)\n .map(part => (part.type === 'decimal' ? sep : part.value))\n .join('')\n}\n\n/**\n * `aria-valuetext` нужен только при группировке: без неё `aria-valuenow` уже\n * несёт то же самое число, и дублировать его текстом незачем.\n */\nconst ariaValueText = computed(() => {\n if (!props.useGrouping)\n return undefined\n return props.modelValue === null ? undefined : formatGrouped(props.modelValue)\n})\n\n/**\n * Что показывает поле: набираемый черновик, пока он есть, иначе — модель.\n * Группировка только вне фокуса: при правке разделители разрядов мешают.\n */\nconst displayValue = computed(() => {\n if (draft.value !== null)\n return draft.value\n if (props.modelValue === null)\n return ''\n if (props.useGrouping && !isFocused.value)\n return formatGrouped(props.modelValue)\n return format(props.modelValue)\n})\n\nfunction onFocus(event: FocusEvent): void {\n isFocused.value = true\n emit('focus', event)\n}\n\nfunction onBlur(event: FocusEvent): void {\n isFocused.value = false\n // `change` до `blur` доходит не всегда (значение могло вернуться к исходному),\n // а черновик обязан сняться в любом случае — иначе поле застынет на нём.\n draft.value = null\n emit('blur', event)\n stopRepeat()\n}\n\n// ————— Удержание кнопки ±: шаг повторяется, пока кнопку держат.\nconst REPEAT_DELAY_MS = 400\nconst REPEAT_INTERVAL_MS = 60\n\nlet repeatDelay: ReturnType<typeof setTimeout> | null = null\nlet repeatTimer: ReturnType<typeof setInterval> | null = null\n\nfunction stopRepeat(): void {\n if (repeatDelay !== null) {\n clearTimeout(repeatDelay)\n repeatDelay = null\n }\n if (repeatTimer !== null) {\n clearInterval(repeatTimer)\n repeatTimer = null\n }\n}\n\nfunction canStep(dir: 1 | -1): boolean {\n return dir === 1 ? canIncrease.value : canDecrease.value\n}\n\n/**\n * `pointerdown`, а не `mousedown`: на тач-устройствах второго не бывает.\n * Повтор останавливается на границе — иначе таймер крутился бы вхолостую.\n */\nfunction startRepeat(dir: 1 | -1): void {\n if (isDisabled.value || isReadonly.value)\n return\n\n stopRepeat()\n repeatFired = false\n repeatDelay = setTimeout(() => {\n repeatTimer = setInterval(() => {\n if (!canStep(dir)) {\n stopRepeat()\n return\n }\n repeatFired = true\n stepBy(dir)\n }, REPEAT_INTERVAL_MS)\n }, REPEAT_DELAY_MS)\n}\n\nonBeforeUnmount(stopRepeat)\n\n// ————— Очистка значения.\nconst resolvedClearable = useGrComponentProp('GrNumberInput', 'clearable', () => props.clearable, false)\nconst resolvedClearLabel = computed(() => props.clearLabel ?? t('gr.input.clear', 'Clear'))\n\nconst clearVisible = computed(() =>\n resolvedClearable.value && !isDisabled.value && !isReadonly.value && props.modelValue !== null,\n)\n\n/**\n * Небуквенный признак состояния: иконка для глаз, скрытая подпись для\n * скринридера (`shared/controlState`). Встаёт левее крестика по той же\n * арифметике — правая зона тут занята суффиксом и кнопками ±, и место\n * приходится отсчитывать, а не полагаться на поток.\n */\nconst signalState = computed(() => controlSignalState(props.state, isInvalid.value))\nconst stateIcon = computed(() => (signalState.value === 'success' ? IconCheckCircle : IconAlertTriangle))\nconst stateIconClass = computed(() => (signalState.value\n ? `${controlStateIconClass} ${controlStateIconColors[signalState.value]}`\n : ''))\nconst stateIconStyle = computed(() => {\n const controls = rightControlsCount.value > 0 ? px(addonPx.value * rightControlsCount.value) : '0px'\n // Крестик 20px плюс зазор: без сдвига признак лёг бы под него.\n const clear = clearVisible.value ? '26px' : '0px'\n\n return { right: addLen(addLen(addLen(suffixLen.value, controls), clear), '6px') }\n})\n\nconst stateText = computed(() => (signalState.value\n ? t(controlStateTextKey[signalState.value], controlStateFallbackText[signalState.value])\n : ''))\n\nconst stateTextId = useId()\nconst describedBy = computed(() =>\n [field?.describedById.value, signalState.value ? stateTextId : undefined]\n .filter(Boolean)\n .join(' ') || undefined,\n)\n\nfunction clear(): void {\n draft.value = null\n emit('update:modelValue', null)\n emit('change', null)\n emit('clear')\n focus()\n}\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (props.modelValue === undefined) {\n console.warn(\n `[granularity] GrNumberInput: обязательный проп \\`modelValue\\` не передан — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <div\n data-gr-number-input\n class=\"relative w-full overflow-hidden rounded-[var(--gr-radius-control)] border bg-[var(--gr-bg)] transition-colors duration-[var(--gr-duration-fast)] focus-within:ring-2 focus-within:ring-[var(--gr-ring)]\"\n :class=\"shellClassName\"\n >\n <div\n v-if=\"$slots.prefix\"\n ref=\"prefixEl\"\n data-testid=\"number-input-prefix\"\n data-gr-number-input-prefix\n class=\"absolute inset-y-0 left-0 flex items-center justify-center border-r border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :style=\"prefixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"prefix\" />\n </div>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n v-bind=\"$attrs\"\n :name=\"name\"\n type=\"text\"\n :inputmode=\"inputmode\"\n :autocomplete=\"autocomplete\"\n :placeholder=\"placeholder\"\n :disabled=\"isDisabled\"\n :value=\"displayValue\"\n role=\"spinbutton\"\n :aria-valuenow=\"ariaValueNow\"\n :aria-valuemin=\"min\"\n :aria-valuemax=\"max\"\n :aria-valuetext=\"ariaValueText\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :readonly=\"isReadonly\"\n class=\"w-full bg-transparent text-[var(--gr-fg)] placeholder:text-[var(--gr-muted-fg)] focus:placeholder:text-transparent focus:outline-none disabled:cursor-not-allowed\"\n :class=\"inputClassName\"\n :style=\"inputStyle\"\n @input=\"onInput\"\n @change=\"onChange\"\n @keydown=\"onKeydown\"\n @focus=\"onFocus\"\n @blur=\"onBlur\"\n >\n\n <div\n v-if=\"$slots.suffix\"\n ref=\"suffixEl\"\n data-testid=\"number-input-suffix\"\n data-gr-number-input-suffix\n class=\"absolute inset-y-0 flex items-center justify-center border-l border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"suffixFixed ? '[direction:rtl]' : ''\"\n :style=\"suffixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"suffix\" />\n </div>\n\n <span\n v-if=\"signalState\"\n data-gr-number-input-state\n class=\"absolute top-1/2 -translate-y-1/2\"\n :class=\"stateIconClass\"\n :style=\"stateIconStyle\"\n aria-hidden=\"true\"\n >\n <component :is=\"stateIcon\" class=\"h-4 w-4\" />\n </span>\n\n <span v-if=\"signalState\" :id=\"stateTextId\" data-gr-number-input-state-text class=\"sr-only\">{{ stateText }}</span>\n\n <button\n v-if=\"clearVisible\"\n type=\"button\"\n data-gr-number-input-clear\n :class=\"clearButtonClass\"\n :style=\"clearButtonStyle\"\n :aria-label=\"resolvedClearLabel\"\n @mousedown.prevent\n @click=\"clear\"\n >\n <GrIcon :size=\"14\">\n <IconX />\n </GrIcon>\n </button>\n\n <div\n v-if=\"hasVerticalControls\"\n data-testid=\"number-input-controls-vertical\"\n data-gr-number-input-controls=\"vertical\"\n class=\"absolute inset-y-0 flex items-center justify-center border-l border-[var(--gr-brd)]\"\n :style=\"verticalControlsStyle\"\n >\n <div class=\"flex flex-col justify-center gap-1\">\n <button\n type=\"button\"\n data-gr-number-input-increase\n :class=\"stepperCompactClass\"\n :disabled=\"isDisabled || isReadonly || !canIncrease\"\n :aria-label=\"resolvedIncreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronUp />\n </GrIcon>\n </button>\n <button\n type=\"button\"\n data-gr-number-input-decrease\n :class=\"stepperCompactClass\"\n :disabled=\"isDisabled || isReadonly || !canDecrease\"\n :aria-label=\"resolvedDecreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(-1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(-1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronDown />\n </GrIcon>\n </button>\n </div>\n </div>\n\n <div\n v-if=\"hasHorizontalControls\"\n data-testid=\"number-input-controls-horizontal-left\"\n data-gr-number-input-controls=\"horizontal-left\"\n class=\"absolute inset-y-0 flex items-stretch justify-center border-r border-[var(--gr-brd)]\"\n :style=\"horizontalLeftControlsStyle\"\n >\n <button\n type=\"button\"\n data-gr-number-input-decrease\n :class=\"stepperWideClass\"\n :disabled=\"isDisabled || isReadonly || !canDecrease\"\n :aria-label=\"resolvedDecreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(-1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(-1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronLeft />\n </GrIcon>\n </button>\n </div>\n\n <div\n v-if=\"hasHorizontalControls\"\n data-testid=\"number-input-controls-horizontal-right\"\n data-gr-number-input-controls=\"horizontal-right\"\n class=\"absolute inset-y-0 flex items-stretch justify-center border-l border-[var(--gr-brd)]\"\n :style=\"horizontalRightControlsStyle\"\n >\n <button\n type=\"button\"\n data-gr-number-input-increase\n :class=\"stepperWideClass\"\n :disabled=\"isDisabled || isReadonly || !canIncrease\"\n :aria-label=\"resolvedIncreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronRight />\n </GrIcon>\n </button>\n </div>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as grNumberInputSafelist, t as grNumberInputConfig } from "../../chunks/config-
|
|
2
|
-
import { t as GrNumberInput_default } from "./chunks/GrNumberInput-
|
|
1
|
+
import { n as grNumberInputSafelist, t as grNumberInputConfig } from "../../chunks/config-BLP94dPY.js";
|
|
2
|
+
import { t as GrNumberInput_default } from "./chunks/GrNumberInput-BI6-vkcs.js";
|
|
3
3
|
export { GrNumberInput_default as GrNumberInput, GrNumberInput_default as default, grNumberInputConfig, grNumberInputSafelist };
|