@feugene/granularity 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/config-BFJIxE8b.js +62 -0
- package/dist/chunks/config-BFJIxE8b.js.map +1 -0
- package/dist/chunks/{config-DBQQT-WP.js → config-BIa3ilQm.js} +18 -3
- package/dist/chunks/config-BIa3ilQm.js.map +1 -0
- package/dist/chunks/config-BSyiFlbi2.js.map +1 -0
- package/dist/chunks/config-BViooYNv2.js.map +1 -0
- package/dist/chunks/config-BoNR7_PD.js.map +1 -0
- package/dist/chunks/{config-Be9k9bOf.js → config-C8tVRv8i.js} +16 -2
- package/dist/chunks/config-C8tVRv8i.js.map +1 -0
- package/dist/chunks/{config-Bm_Sovla.js → config-DYgfMrIF.js} +14 -2
- package/dist/chunks/config-DYgfMrIF.js.map +1 -0
- package/dist/chunks/{config-DCwQBMmJ.js → config-rkcKArZQ2.js} +27 -8
- package/dist/chunks/config-rkcKArZQ2.js.map +1 -0
- package/dist/chunks/config-vSji8w902.js.map +1 -0
- package/dist/chunks/{generated-CiCrGDQO.js → generated-1WEi10KK.js} +89 -6
- package/dist/chunks/{generated-CiCrGDQO.js.map → generated-1WEi10KK.js.map} +1 -1
- package/dist/chunks/{granular-provider-CQKhl5C_.js → granular-provider-Dr2PKgl1.js} +13 -11
- package/dist/chunks/granular-provider-Dr2PKgl1.js.map +1 -0
- package/dist/components/GrCard/chunks/{GrCard-Bc7nRfbe.js → GrCard-gI2wA8ZZ.js} +8 -2
- package/dist/components/GrCard/chunks/GrCard-gI2wA8ZZ.js.map +1 -0
- package/dist/components/GrCard/index.js +2 -2
- package/dist/components/GrCollapse/chunks/{GrCollapse-C1rCfbc0.js → GrCollapse-DIHEiTju.js} +1 -1
- package/dist/components/GrCollapse/chunks/{GrCollapse-C1rCfbc0.js.map → GrCollapse-DIHEiTju.js.map} +1 -1
- package/dist/components/GrCollapse/index.js +1 -1
- package/dist/components/GrDelta/chunks/{GrDelta-DFvxNpmN.js → GrDelta-NE5HhXoo.js} +0 -0
- package/dist/components/GrDelta/chunks/GrDelta-NE5HhXoo.js.map +1 -0
- package/dist/components/GrDelta/index.js +2 -2
- package/dist/components/GrDescriptionList/chunks/{GrDescriptionList-2zFXzX83.js → GrDescriptionList-C6Pxjjbh.js} +8 -4
- package/dist/components/GrDescriptionList/chunks/GrDescriptionList-C6Pxjjbh.js.map +1 -0
- package/dist/components/GrDescriptionList/index.js +2 -2
- package/dist/components/GrDropdown/index.js +1 -1
- package/dist/components/GrEmptyState/chunks/{GrEmptyState-CS9do8eM.js → GrEmptyState-BGUX8O0U.js} +1 -1
- package/dist/components/GrEmptyState/chunks/{GrEmptyState-CS9do8eM.js.map → GrEmptyState-BGUX8O0U.js.map} +1 -1
- package/dist/components/GrEmptyState/index.js +2 -2
- package/dist/components/GrList/chunks/{GrList-BRYhpqIe.js → GrList-DAtzVIHh.js} +1 -1
- package/dist/components/GrList/chunks/{GrList-BRYhpqIe.js.map → GrList-DAtzVIHh.js.map} +1 -1
- package/dist/components/GrList/index.js +1 -1
- package/dist/components/GrLoading/chunks/{GrLoading-C3DZSSuh.js → GrLoading-DeBFTvFX.js} +1 -1
- package/dist/components/GrLoading/chunks/GrLoading-DeBFTvFX.js.map +1 -0
- package/dist/components/GrLoading/index.js +1 -1
- package/dist/components/GrPopover/index.js +1 -1
- package/dist/components/GrSegmented/chunks/{GrSegmented-D1mjxF_O.js → GrSegmented-DKpo3cnC.js} +30 -5
- package/dist/components/GrSegmented/chunks/GrSegmented-DKpo3cnC.js.map +1 -0
- package/dist/components/GrSegmented/index.js +1 -1
- package/dist/components/GrSortableList/chunks/{GrSortableList-DPjxilmN.js → GrSortableList-BmcUVAMb.js} +1 -1
- package/dist/components/GrSortableList/chunks/{GrSortableList-DPjxilmN.js.map → GrSortableList-BmcUVAMb.js.map} +1 -1
- package/dist/components/GrSortableList/index.js +1 -1
- package/dist/components/GrStatistic/chunks/{GrStatistic-C7wQECeS.js → GrStatistic-BJQv5aOS.js} +41 -26
- package/dist/components/GrStatistic/chunks/GrStatistic-BJQv5aOS.js.map +1 -0
- package/dist/components/GrStatistic/index.js +2 -2
- package/dist/components/GrSwitch/chunks/{GrSwitch-x_vQQ6ao.js → GrSwitch-CHiIj9GT.js} +1 -1
- package/dist/components/GrSwitch/chunks/{GrSwitch-x_vQQ6ao.js.map → GrSwitch-CHiIj9GT.js.map} +1 -1
- package/dist/components/GrSwitch/index.js +2 -2
- package/dist/components/GrValue/chunks/GrValue-CZsuZ4x-.js +38 -0
- package/dist/components/GrValue/chunks/GrValue-CZsuZ4x-.js.map +1 -0
- package/dist/components/GrValue/index.js +3 -0
- package/dist/directives/loading.js +1 -1
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/index.js +22 -20
- package/dist/styles/themes/dark.css +11 -6
- package/dist/styles/themes/light.css +5 -0
- package/dist/theme.js +1 -1
- package/dist/tokens.js +1 -1
- package/dist/types/components/GrCard/grCardStyles.d.ts +11 -0
- package/dist/types/components/GrDelta/grDeltaStyles.d.ts +13 -1
- package/dist/types/components/GrDescriptionList/grDescriptionListStyles.d.ts +29 -1
- package/dist/types/components/GrStatistic/GrStatistic.vue.d.ts +5 -5
- package/dist/types/components/GrStatistic/grStatisticStyles.d.ts +9 -0
- package/dist/types/components/GrValue/GrValue.vue.d.ts +51 -0
- package/dist/types/components/GrValue/config.d.ts +1 -0
- package/dist/types/components/GrValue/grValueStyles.d.ts +34 -0
- package/dist/types/components/GrValue/index.d.ts +5 -0
- package/dist/types/components/GrValue/safelist.d.ts +1 -0
- package/dist/types/components/shared/truncationTitle.d.ts +16 -0
- package/dist/types/granular-provider/shared.d.ts +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/web-types.json +56 -1
- package/package.json +5 -1
- package/tokens/themes/dark.json +26 -6
- package/tokens/themes/light.json +20 -0
- package/dist/chunks/config-BSyiFlbi.js.map +0 -1
- package/dist/chunks/config-BViooYNv.js.map +0 -1
- package/dist/chunks/config-Be9k9bOf.js.map +0 -1
- package/dist/chunks/config-Bm_Sovla.js.map +0 -1
- package/dist/chunks/config-BoNR7_PD2.js.map +0 -1
- package/dist/chunks/config-DBQQT-WP.js.map +0 -1
- package/dist/chunks/config-DCwQBMmJ.js.map +0 -1
- package/dist/chunks/config-vSji8w90.js.map +0 -1
- package/dist/chunks/granular-provider-CQKhl5C_.js.map +0 -1
- package/dist/components/GrCard/chunks/GrCard-Bc7nRfbe.js.map +0 -1
- package/dist/components/GrDelta/chunks/GrDelta-DFvxNpmN.js.map +0 -1
- package/dist/components/GrDescriptionList/chunks/GrDescriptionList-2zFXzX83.js.map +0 -1
- package/dist/components/GrLoading/chunks/GrLoading-C3DZSSuh.js.map +0 -1
- package/dist/components/GrSegmented/chunks/GrSegmented-D1mjxF_O.js.map +0 -1
- package/dist/components/GrStatistic/chunks/GrStatistic-C7wQECeS.js.map +0 -1
- /package/dist/chunks/{config-BSyiFlbi.js → config-BSyiFlbi2.js} +0 -0
- /package/dist/chunks/{config-BViooYNv.js → config-BViooYNv2.js} +0 -0
- /package/dist/chunks/{config-BoNR7_PD2.js → config-BoNR7_PD.js} +0 -0
- /package/dist/chunks/{config-vSji8w90.js → config-vSji8w902.js} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrDelta-DFvxNpmN.js","names":[],"sources":["../../../../src/internal/granularityFormat.ts","../../../../src/components/GrDelta/GrDelta.vue","../../../../src/components/GrDelta/GrDelta.vue"],"sourcesContent":["/**\n * Форматирование чисел и дат по локали.\n *\n * Своё, а не из `@feugene/fint-i18n`: тот объявлен **опциональной** peer-зависимостью,\n * и пакет обязан работать без него. Под капотом всё равно `Intl`, то есть\n * результат совпадает с `useI18nFormat()` библиотеки — расхождения между\n * приложением с i18n и без него не возникает.\n *\n * Инстансы `Intl` кэшируются: их построение примерно на порядок дороже самого\n * форматирования. Кэш ограничен и сбрасывается целиком — иначе сгенерированные\n * на лету опции растили бы его без предела.\n */\n\nconst CACHE_LIMIT = 64\n\nconst numberFormats = new Map<string, Intl.NumberFormat>()\nconst dateFormats = new Map<string, Intl.DateTimeFormat>()\n\nfunction cacheKey(locale: string, options?: object): string {\n return options ? `${locale}\u0000${JSON.stringify(options)}` : locale\n}\n\nfunction take<T>(cache: Map<string, T>, key: string, create: () => T): T {\n const cached = cache.get(key)\n if (cached) return cached\n\n const created = create()\n if (cache.size >= CACHE_LIMIT) cache.clear()\n cache.set(key, created)\n\n return created\n}\n\n/**\n * Локаль приходит из настроек приложения, а `Intl` бросает на некорректном\n * теге. Форматирование не имеет права уронить рендер, поэтому неизвестный тег\n * сводим к `en`.\n */\nexport function formatNumber(\n locale: string,\n value: number | bigint,\n options?: Intl.NumberFormatOptions,\n): string {\n try {\n return take(numberFormats, cacheKey(locale, options), () => new Intl.NumberFormat(locale, options)).format(value)\n }\n catch {\n return take(numberFormats, cacheKey('en', options), () => new Intl.NumberFormat('en', options)).format(value)\n }\n}\n\n/**\n * Те же правила, что у `formatNumber`, но по частям: вызывающему нужно знать,\n * где в записи стоит знак, чтобы вынести его отдельным узлом.\n *\n * Инстанс тот же, что у `formatNumber`: `Intl.NumberFormat` умеет оба метода, а\n * второй кэш под теми же ключами разъехался бы с первым на первом вытеснении.\n */\nexport function formatNumberToParts(\n locale: string,\n value: number | bigint,\n options?: Intl.NumberFormatOptions,\n): Intl.NumberFormatPart[] {\n try {\n return take(numberFormats, cacheKey(locale, options), () => new Intl.NumberFormat(locale, options)).formatToParts(value)\n }\n catch {\n return take(numberFormats, cacheKey('en', options), () => new Intl.NumberFormat('en', options)).formatToParts(value)\n }\n}\n\n/**\n * Ведущий знак и остаток записи.\n *\n * Нужно там, где знаку полагается свой узел разметки: валюта обязана стоять\n * после знака (`+$0,03`, а не `$+0,03`). Резать готовую строку по первому\n * символу нельзя — в RTL-локалях знаку предшествует невидимая метка\n * направления (`he-IL` — U+200E, `ar-EG` — U+061C), которой и разворачивается\n * минус относительно цифр. Метка уходит вместе со знаком: она управляет им.\n *\n * `sign` пуст, когда знака нет вовсе (ноль, положительное при `signDisplay:\n * 'auto'`, `NaN`) либо когда перед ним стоит содержательная часть. Во втором\n * случае запись не делится совсем: вернуть половину по догадке хуже, чем\n * вернуть целое.\n */\nexport function splitLeadingSign(\n parts: readonly Intl.NumberFormatPart[],\n): { sign: string, rest: string } {\n const join = (from: readonly Intl.NumberFormatPart[]): string => from.map(part => part.value).join('')\n const at = parts.findIndex(part => part.type === 'minusSign' || part.type === 'plusSign')\n\n if (at === -1 || parts.slice(0, at).some(part => part.type !== 'literal'))\n return { sign: '', rest: join(parts) }\n\n return { sign: join(parts.slice(0, at + 1)), rest: join(parts.slice(at + 1)) }\n}\n\nexport function formatDate(\n locale: string,\n value: Date | number | string,\n options?: Intl.DateTimeFormatOptions,\n): string {\n const date = value instanceof Date ? value : new Date(value)\n\n // Невалидную дату возвращаем как есть: пустая или «Invalid Date» в интерфейсе\n // хуже, чем исходное значение, но лучше, чем исключение на рендере.\n if (Number.isNaN(date.getTime())) return String(value)\n\n try {\n return take(dateFormats, cacheKey(locale, options), () => new Intl.DateTimeFormat(locale, options)).format(date)\n }\n catch {\n return take(dateFormats, cacheKey('en', options), () => new Intl.DateTimeFormat('en', options)).format(date)\n }\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport { formatNumberToParts, splitLeadingSign } from '../../internal/granularityFormat'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\n\nimport IconMinus from '~icons/lucide/minus'\nimport IconTrendingDown from '~icons/lucide/trending-down'\nimport IconTrendingUp from '~icons/lucide/trending-up'\n\nimport { deltaDirection, deltaTone, type GrDeltaPolarity, type GrDeltaTone } from './deltaTone'\nimport {\n deltaArrowClass,\n deltaEmptyClass,\n deltaRootClass,\n deltaSizeClass,\n deltaSuffixClass,\n deltaToneClass,\n} from './grDeltaStyles'\n\nimport type { GrComponentSize } from '../shared/sizes'\n\nexport type { GrDeltaPolarity, GrDeltaTone } from './deltaTone'\n\n/**\n * Величина со знаком и тоном **внутри строки текста**: «Дельта: +$0.004».\n *\n * Компонент презентационный: дельту не считает и периоды не сравнивает —\n * «к чему сравнивать» знает приложение. Блочная плитка показателя — `GrStatistic`.\n */\nexport interface GrDeltaProps {\n /** Величина. `null` — её нет; это не ноль. */\n value: number | null\n /** Знаков после запятой. Не задано — как отдаст локаль. */\n precision?: number\n /** Приписка перед числом: валюта, единица. */\n prefix?: string\n /** Приписка после числа: `%`, `мс`. */\n suffix?: string\n /**\n * Что считать хорошим. Для выручки рост — успех, для себестоимости и времени\n * отклика — наоборот; без этого компонент врал бы в половине случаев.\n */\n polarity?: GrDeltaPolarity\n /** Ставить `+` у положительных. Минус подставляет `Intl` сам. */\n showSign?: boolean\n /** Стрелка направления. Декоративна: направление уже в знаке. */\n showArrow?: boolean\n /** Чем печатать отсутствующую величину. */\n emptyText?: string\n /** Локаль форматирования. Не задана — локаль i18n-адаптера. */\n locale?: string\n size?: GrComponentSize\n}\n\nconst props = withDefaults(defineProps<GrDeltaProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n precision: undefined,\n prefix: undefined,\n suffix: undefined,\n polarity: undefined,\n showSign: undefined,\n showArrow: undefined,\n emptyText: undefined,\n locale: undefined,\n size: undefined,\n})\n\nconst { locale } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrDelta' })\nconst resolvedLocale = computed(() => props.locale ?? locale.value ?? 'en')\nconst resolvedPolarity = useGrComponentProp('GrDelta', 'polarity', () => props.polarity, 'positive-good')\nconst resolvedShowSign = useGrComponentProp('GrDelta', 'showSign', () => props.showSign, true)\nconst resolvedShowArrow = useGrComponentProp('GrDelta', 'showArrow', () => props.showArrow, false)\nconst resolvedEmptyText = useGrComponentProp('GrDelta', 'emptyText', () => props.emptyText, '—')\n\nconst isEmpty = computed(() => props.value === null || props.value === undefined)\n\nconst tone = computed<GrDeltaTone>(() => (\n isEmpty.value ? 'neutral' : deltaTone(props.value as number, resolvedPolarity.value)\n))\n\nconst direction = computed(() => (isEmpty.value ? 'flat' : deltaDirection(props.value as number)))\n\nconst formatOptions = computed<Intl.NumberFormatOptions>(() => ({\n signDisplay: resolvedShowSign.value ? 'exceptZero' : 'auto',\n ...(props.precision === undefined\n ? {}\n : { minimumFractionDigits: props.precision, maximumFractionDigits: props.precision }),\n}))\n\n/**\n * Знак и число по отдельности.\n *\n * Знак — префикс форматирования, а не склейка строки. `'+' + value` уже ломало\n * `GrStatistic`: строка `'+10'` разбирается как число, и разряды теряются.\n * Поэтому знак не дописывают, а **вынимают** из готовых частей `Intl`.\n *\n * Делится величина только вместе с `prefix`: валюта обязана стоять после знака\n * (`+$0,0280`, а не `$+0,0280`). Без приписки делить нечего — знак остаётся\n * внутри величины, и разметка не меняется ни на байт.\n */\nconst valueNodes = computed<{ sign: string, value: string }>(() => {\n if (isEmpty.value) return { sign: '', value: resolvedEmptyText.value }\n\n const parts = formatNumberToParts(resolvedLocale.value, props.value as number, formatOptions.value)\n\n if (!props.prefix) return { sign: '', value: parts.map(part => part.value).join('') }\n\n const { sign, rest } = splitLeadingSign(parts)\n\n return { sign, value: rest }\n})\n\nconst sign = computed(() => valueNodes.value.sign)\nconst formatted = computed(() => valueNodes.value.value)\n\nconst arrowIcon = computed(() => {\n if (direction.value === 'up') return IconTrendingUp\n if (direction.value === 'down') return IconTrendingDown\n return IconMinus\n})\n\nconst rootClass = computed(() => [\n deltaRootClass,\n deltaSizeClass[resolvedSize.value],\n isEmpty.value ? deltaEmptyClass : deltaToneClass[tone.value],\n].filter(Boolean).join(' '))\n</script>\n\n<template>\n <span data-gr-delta :data-tone=\"isEmpty ? undefined : tone\" :class=\"rootClass\">\n <component\n :is=\"arrowIcon\"\n v-if=\"resolvedShowArrow && !isEmpty\"\n data-gr-delta-arrow\n :class=\"deltaArrowClass\"\n aria-hidden=\"true\"\n />\n\n <span v-if=\"sign\" data-gr-delta-sign>{{ sign }}</span>\n <span v-if=\"prefix && !isEmpty\" data-gr-delta-prefix>{{ prefix }}</span>\n <span data-gr-delta-value>{{ formatted }}</span>\n <span v-if=\"suffix && !isEmpty\" data-gr-delta-suffix :class=\"deltaSuffixClass\">{{ suffix }}</span>\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport { formatNumberToParts, splitLeadingSign } from '../../internal/granularityFormat'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\n\nimport IconMinus from '~icons/lucide/minus'\nimport IconTrendingDown from '~icons/lucide/trending-down'\nimport IconTrendingUp from '~icons/lucide/trending-up'\n\nimport { deltaDirection, deltaTone, type GrDeltaPolarity, type GrDeltaTone } from './deltaTone'\nimport {\n deltaArrowClass,\n deltaEmptyClass,\n deltaRootClass,\n deltaSizeClass,\n deltaSuffixClass,\n deltaToneClass,\n} from './grDeltaStyles'\n\nimport type { GrComponentSize } from '../shared/sizes'\n\nexport type { GrDeltaPolarity, GrDeltaTone } from './deltaTone'\n\n/**\n * Величина со знаком и тоном **внутри строки текста**: «Дельта: +$0.004».\n *\n * Компонент презентационный: дельту не считает и периоды не сравнивает —\n * «к чему сравнивать» знает приложение. Блочная плитка показателя — `GrStatistic`.\n */\nexport interface GrDeltaProps {\n /** Величина. `null` — её нет; это не ноль. */\n value: number | null\n /** Знаков после запятой. Не задано — как отдаст локаль. */\n precision?: number\n /** Приписка перед числом: валюта, единица. */\n prefix?: string\n /** Приписка после числа: `%`, `мс`. */\n suffix?: string\n /**\n * Что считать хорошим. Для выручки рост — успех, для себестоимости и времени\n * отклика — наоборот; без этого компонент врал бы в половине случаев.\n */\n polarity?: GrDeltaPolarity\n /** Ставить `+` у положительных. Минус подставляет `Intl` сам. */\n showSign?: boolean\n /** Стрелка направления. Декоративна: направление уже в знаке. */\n showArrow?: boolean\n /** Чем печатать отсутствующую величину. */\n emptyText?: string\n /** Локаль форматирования. Не задана — локаль i18n-адаптера. */\n locale?: string\n size?: GrComponentSize\n}\n\nconst props = withDefaults(defineProps<GrDeltaProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n precision: undefined,\n prefix: undefined,\n suffix: undefined,\n polarity: undefined,\n showSign: undefined,\n showArrow: undefined,\n emptyText: undefined,\n locale: undefined,\n size: undefined,\n})\n\nconst { locale } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrDelta' })\nconst resolvedLocale = computed(() => props.locale ?? locale.value ?? 'en')\nconst resolvedPolarity = useGrComponentProp('GrDelta', 'polarity', () => props.polarity, 'positive-good')\nconst resolvedShowSign = useGrComponentProp('GrDelta', 'showSign', () => props.showSign, true)\nconst resolvedShowArrow = useGrComponentProp('GrDelta', 'showArrow', () => props.showArrow, false)\nconst resolvedEmptyText = useGrComponentProp('GrDelta', 'emptyText', () => props.emptyText, '—')\n\nconst isEmpty = computed(() => props.value === null || props.value === undefined)\n\nconst tone = computed<GrDeltaTone>(() => (\n isEmpty.value ? 'neutral' : deltaTone(props.value as number, resolvedPolarity.value)\n))\n\nconst direction = computed(() => (isEmpty.value ? 'flat' : deltaDirection(props.value as number)))\n\nconst formatOptions = computed<Intl.NumberFormatOptions>(() => ({\n signDisplay: resolvedShowSign.value ? 'exceptZero' : 'auto',\n ...(props.precision === undefined\n ? {}\n : { minimumFractionDigits: props.precision, maximumFractionDigits: props.precision }),\n}))\n\n/**\n * Знак и число по отдельности.\n *\n * Знак — префикс форматирования, а не склейка строки. `'+' + value` уже ломало\n * `GrStatistic`: строка `'+10'` разбирается как число, и разряды теряются.\n * Поэтому знак не дописывают, а **вынимают** из готовых частей `Intl`.\n *\n * Делится величина только вместе с `prefix`: валюта обязана стоять после знака\n * (`+$0,0280`, а не `$+0,0280`). Без приписки делить нечего — знак остаётся\n * внутри величины, и разметка не меняется ни на байт.\n */\nconst valueNodes = computed<{ sign: string, value: string }>(() => {\n if (isEmpty.value) return { sign: '', value: resolvedEmptyText.value }\n\n const parts = formatNumberToParts(resolvedLocale.value, props.value as number, formatOptions.value)\n\n if (!props.prefix) return { sign: '', value: parts.map(part => part.value).join('') }\n\n const { sign, rest } = splitLeadingSign(parts)\n\n return { sign, value: rest }\n})\n\nconst sign = computed(() => valueNodes.value.sign)\nconst formatted = computed(() => valueNodes.value.value)\n\nconst arrowIcon = computed(() => {\n if (direction.value === 'up') return IconTrendingUp\n if (direction.value === 'down') return IconTrendingDown\n return IconMinus\n})\n\nconst rootClass = computed(() => [\n deltaRootClass,\n deltaSizeClass[resolvedSize.value],\n isEmpty.value ? deltaEmptyClass : deltaToneClass[tone.value],\n].filter(Boolean).join(' '))\n</script>\n\n<template>\n <span data-gr-delta :data-tone=\"isEmpty ? undefined : tone\" :class=\"rootClass\">\n <component\n :is=\"arrowIcon\"\n v-if=\"resolvedShowArrow && !isEmpty\"\n data-gr-delta-arrow\n :class=\"deltaArrowClass\"\n aria-hidden=\"true\"\n />\n\n <span v-if=\"sign\" data-gr-delta-sign>{{ sign }}</span>\n <span v-if=\"prefix && !isEmpty\" data-gr-delta-prefix>{{ prefix }}</span>\n <span data-gr-delta-value>{{ formatted }}</span>\n <span v-if=\"suffix && !isEmpty\" data-gr-delta-suffix :class=\"deltaSuffixClass\">{{ suffix }}</span>\n </span>\n</template>\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrDescriptionList-2zFXzX83.js","names":[],"sources":["../../../../src/components/GrDescriptionList/GrDescriptionList.vue","../../../../src/components/GrDescriptionList/GrDescriptionList.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, useSlots } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\n\nimport {\n descriptionColumnsClass,\n descriptionDensityClass,\n descriptionDividedClass,\n descriptionLabelInlineClass,\n descriptionLabelStackedClass,\n descriptionPairFlowClass,\n descriptionPairInlineClass,\n descriptionPairStackedClass,\n descriptionRootClass,\n descriptionRootFlowClass,\n descriptionRootGridClass,\n descriptionSizeClass,\n descriptionValueClass,\n descriptionValueToneClass,\n type GrDescriptionColumns,\n type GrDescriptionDensity,\n type GrDescriptionLayout,\n} from './grDescriptionListStyles'\n\nimport type { GrComponentSize } from '../shared/sizes'\nimport type { GrTone } from '../shared/tones'\n\nexport type {\n GrDescriptionColumns,\n GrDescriptionDensity,\n GrDescriptionLayout,\n} from './grDescriptionListStyles'\n\nexport interface GrDescriptionItem {\n label: string\n /** Значение. `null` и `''` печатаются прочерком — строка не исчезает. */\n value?: string | number | null\n /** Ключ слотов `#value-<name>` и `#label-<name>`. */\n name?: string\n /** Тон значения. Подпись остаётся нейтральной всегда. */\n tone?: GrTone\n}\n\n/**\n * Список пар «характеристика → значение»: настоящий `<dl>` с выравниванием\n * значений в колонку.\n *\n * В отличие от `GrList`, где пункт — это «заголовок с подзаголовком», здесь\n * два разных по роли элемента: термин и его значение.\n */\nexport interface GrDescriptionListProps {\n items: readonly GrDescriptionItem[]\n /**\n * `inline` — подпись слева колонкой; `stacked` — над значением; `flow` —\n * пары текут по строке с переносом (метаданные внутри пункта списка).\n */\n layout?: GrDescriptionLayout\n /** Ширина колонки подписей при `inline`. */\n labelWidth?: string\n /**\n * Ниже какой ширины контейнера (px) `inline` переключается на `stacked`.\n * В узкой колонке фиксированная подпись выжимает значение в букву на строку.\n */\n stackBelow?: number\n density?: GrDescriptionDensity\n /** Отбивать пары линиями. В `flow` не применяется. */\n divided?: boolean\n /** Колонки сетки. В `flow` не применяются: строка колонок не имеет. */\n columns?: GrDescriptionColumns\n /** Чем печатать пустое значение. */\n emptyText?: string\n size?: GrComponentSize\n}\n\nconst props = withDefaults(defineProps<GrDescriptionListProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n layout: undefined,\n labelWidth: undefined,\n stackBelow: undefined,\n density: undefined,\n divided: undefined,\n columns: undefined,\n emptyText: undefined,\n size: undefined,\n})\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrDescriptionList' })\nconst resolvedLayout = useGrComponentProp('GrDescriptionList', 'layout', () => props.layout, 'inline')\nconst resolvedDensity = useGrComponentProp('GrDescriptionList', 'density', () => props.density, 'regular')\nconst resolvedDivided = useGrComponentProp('GrDescriptionList', 'divided', () => props.divided, false)\nconst resolvedColumns = useGrComponentProp('GrDescriptionList', 'columns', () => props.columns, 1)\nconst resolvedLabelWidth = useGrComponentProp('GrDescriptionList', 'labelWidth', () => props.labelWidth, '11rem')\nconst resolvedEmptyText = useGrComponentProp('GrDescriptionList', 'emptyText', () => props.emptyText, '—')\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst narrow = ref(false)\n\n/**\n * `stackBelow` меряет контейнер, а не вьюпорт: пары живут и в узкой карточке\n * на широком экране. Container query здесь не подходит — порог приходит числом\n * в пропе, а `var()` в условии запроса не работает.\n */\nlet observer: ResizeObserver | null = null\n\nonMounted(() => {\n if (props.stackBelow === undefined || typeof ResizeObserver === 'undefined') return\n\n observer = new ResizeObserver(([entry]) => {\n narrow.value = entry.contentRect.width < (props.stackBelow as number)\n })\n if (rootEl.value) observer.observe(rootEl.value)\n})\n\nonBeforeUnmount(() => {\n observer?.disconnect()\n observer = null\n})\n\n/**\n * До монтирования действует заданная раскладка — это же и есть серверный рендер.\n *\n * Порог спасает только `inline`: колонка подписей в узком контейнере выжимает\n * значение в букву на строку. `flow` переносится сам, и подменять его нечем.\n */\nconst effectiveLayout = computed<GrDescriptionLayout>(() => (\n narrow.value && resolvedLayout.value === 'inline' ? 'stacked' : resolvedLayout.value\n))\n\nconst isInline = computed(() => effectiveLayout.value === 'inline')\nconst isFlow = computed(() => effectiveLayout.value === 'flow')\n\nconst rootClass = computed(() => [\n descriptionRootClass,\n isFlow.value ? descriptionRootFlowClass : descriptionRootGridClass,\n // Колонки принадлежат сетке: в строчной раскладке их задавать нечему.\n isFlow.value ? '' : descriptionColumnsClass[resolvedColumns.value],\n descriptionSizeClass[resolvedSize.value],\n descriptionDensityClass[resolvedDensity.value],\n].filter(Boolean).join(' '))\n\nconst pairClass = computed(() => {\n if (isFlow.value) return descriptionPairFlowClass\n\n return [\n isInline.value ? descriptionPairInlineClass : descriptionPairStackedClass,\n resolvedDivided.value ? descriptionDividedClass : '',\n ].filter(Boolean).join(' ')\n})\n\nconst labelClass = computed(() => (\n isInline.value ? descriptionLabelInlineClass : descriptionLabelStackedClass\n))\n\nconst labelStyle = computed(() => (isInline.value ? { width: resolvedLabelWidth.value } : undefined))\n\n/**\n * Пустое значение печатается прочерком, а не пропускается: «поле есть, значения\n * нет» и «поля нет» — разные утверждения, и выпавшая строка ломает и\n * выравнивание, и чтение.\n */\nfunction isEmptyValue(item: GrDescriptionItem): boolean {\n return item.value === null || item.value === undefined || item.value === ''\n}\n\nfunction valueClass(item: GrDescriptionItem): string {\n return [\n descriptionValueClass,\n item.tone ? descriptionValueToneClass[item.tone] : '',\n ].filter(Boolean).join(' ')\n}\n\nfunction pairKey(item: GrDescriptionItem, index: number): string {\n return item.name ?? `${item.label}-${index}`\n}\n\nfunction hasSlot(name: string): boolean {\n return Boolean(slots[name])\n}\n</script>\n\n<template>\n <dl ref=\"rootEl\" data-gr-description-list :class=\"rootClass\">\n <!-- Пара оборачивается в `<div>`: `dl > div > dt + dd` валиден по HTML5 и\n нужен для раскладки. Голые `<div>` вместо `dt`/`dd` — то, ради чего\n компонент и заводится. -->\n <div\n v-for=\"(item, index) in items\"\n :key=\"pairKey(item, index)\"\n data-gr-description-pair\n :class=\"pairClass\"\n >\n <dt data-gr-description-label :class=\"labelClass\" :style=\"labelStyle\">\n <slot v-if=\"item.name && hasSlot(`label-${item.name}`)\" :name=\"`label-${item.name}`\" :item=\"item\" />\n <template v-else>\n {{ item.label }}\n </template>\n </dt>\n\n <dd data-gr-description-value :class=\"valueClass(item)\">\n <slot v-if=\"item.name && hasSlot(`value-${item.name}`)\" :name=\"`value-${item.name}`\" :item=\"item\" />\n <template v-else-if=\"isEmptyValue(item)\">\n <slot name=\"empty\" :item=\"item\">\n {{ resolvedEmptyText }}\n </slot>\n </template>\n <template v-else>\n {{ item.value }}\n </template>\n </dd>\n </div>\n </dl>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, useSlots } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\n\nimport {\n descriptionColumnsClass,\n descriptionDensityClass,\n descriptionDividedClass,\n descriptionLabelInlineClass,\n descriptionLabelStackedClass,\n descriptionPairFlowClass,\n descriptionPairInlineClass,\n descriptionPairStackedClass,\n descriptionRootClass,\n descriptionRootFlowClass,\n descriptionRootGridClass,\n descriptionSizeClass,\n descriptionValueClass,\n descriptionValueToneClass,\n type GrDescriptionColumns,\n type GrDescriptionDensity,\n type GrDescriptionLayout,\n} from './grDescriptionListStyles'\n\nimport type { GrComponentSize } from '../shared/sizes'\nimport type { GrTone } from '../shared/tones'\n\nexport type {\n GrDescriptionColumns,\n GrDescriptionDensity,\n GrDescriptionLayout,\n} from './grDescriptionListStyles'\n\nexport interface GrDescriptionItem {\n label: string\n /** Значение. `null` и `''` печатаются прочерком — строка не исчезает. */\n value?: string | number | null\n /** Ключ слотов `#value-<name>` и `#label-<name>`. */\n name?: string\n /** Тон значения. Подпись остаётся нейтральной всегда. */\n tone?: GrTone\n}\n\n/**\n * Список пар «характеристика → значение»: настоящий `<dl>` с выравниванием\n * значений в колонку.\n *\n * В отличие от `GrList`, где пункт — это «заголовок с подзаголовком», здесь\n * два разных по роли элемента: термин и его значение.\n */\nexport interface GrDescriptionListProps {\n items: readonly GrDescriptionItem[]\n /**\n * `inline` — подпись слева колонкой; `stacked` — над значением; `flow` —\n * пары текут по строке с переносом (метаданные внутри пункта списка).\n */\n layout?: GrDescriptionLayout\n /** Ширина колонки подписей при `inline`. */\n labelWidth?: string\n /**\n * Ниже какой ширины контейнера (px) `inline` переключается на `stacked`.\n * В узкой колонке фиксированная подпись выжимает значение в букву на строку.\n */\n stackBelow?: number\n density?: GrDescriptionDensity\n /** Отбивать пары линиями. В `flow` не применяется. */\n divided?: boolean\n /** Колонки сетки. В `flow` не применяются: строка колонок не имеет. */\n columns?: GrDescriptionColumns\n /** Чем печатать пустое значение. */\n emptyText?: string\n size?: GrComponentSize\n}\n\nconst props = withDefaults(defineProps<GrDescriptionListProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n layout: undefined,\n labelWidth: undefined,\n stackBelow: undefined,\n density: undefined,\n divided: undefined,\n columns: undefined,\n emptyText: undefined,\n size: undefined,\n})\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrDescriptionList' })\nconst resolvedLayout = useGrComponentProp('GrDescriptionList', 'layout', () => props.layout, 'inline')\nconst resolvedDensity = useGrComponentProp('GrDescriptionList', 'density', () => props.density, 'regular')\nconst resolvedDivided = useGrComponentProp('GrDescriptionList', 'divided', () => props.divided, false)\nconst resolvedColumns = useGrComponentProp('GrDescriptionList', 'columns', () => props.columns, 1)\nconst resolvedLabelWidth = useGrComponentProp('GrDescriptionList', 'labelWidth', () => props.labelWidth, '11rem')\nconst resolvedEmptyText = useGrComponentProp('GrDescriptionList', 'emptyText', () => props.emptyText, '—')\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst narrow = ref(false)\n\n/**\n * `stackBelow` меряет контейнер, а не вьюпорт: пары живут и в узкой карточке\n * на широком экране. Container query здесь не подходит — порог приходит числом\n * в пропе, а `var()` в условии запроса не работает.\n */\nlet observer: ResizeObserver | null = null\n\nonMounted(() => {\n if (props.stackBelow === undefined || typeof ResizeObserver === 'undefined') return\n\n observer = new ResizeObserver(([entry]) => {\n narrow.value = entry.contentRect.width < (props.stackBelow as number)\n })\n if (rootEl.value) observer.observe(rootEl.value)\n})\n\nonBeforeUnmount(() => {\n observer?.disconnect()\n observer = null\n})\n\n/**\n * До монтирования действует заданная раскладка — это же и есть серверный рендер.\n *\n * Порог спасает только `inline`: колонка подписей в узком контейнере выжимает\n * значение в букву на строку. `flow` переносится сам, и подменять его нечем.\n */\nconst effectiveLayout = computed<GrDescriptionLayout>(() => (\n narrow.value && resolvedLayout.value === 'inline' ? 'stacked' : resolvedLayout.value\n))\n\nconst isInline = computed(() => effectiveLayout.value === 'inline')\nconst isFlow = computed(() => effectiveLayout.value === 'flow')\n\nconst rootClass = computed(() => [\n descriptionRootClass,\n isFlow.value ? descriptionRootFlowClass : descriptionRootGridClass,\n // Колонки принадлежат сетке: в строчной раскладке их задавать нечему.\n isFlow.value ? '' : descriptionColumnsClass[resolvedColumns.value],\n descriptionSizeClass[resolvedSize.value],\n descriptionDensityClass[resolvedDensity.value],\n].filter(Boolean).join(' '))\n\nconst pairClass = computed(() => {\n if (isFlow.value) return descriptionPairFlowClass\n\n return [\n isInline.value ? descriptionPairInlineClass : descriptionPairStackedClass,\n resolvedDivided.value ? descriptionDividedClass : '',\n ].filter(Boolean).join(' ')\n})\n\nconst labelClass = computed(() => (\n isInline.value ? descriptionLabelInlineClass : descriptionLabelStackedClass\n))\n\nconst labelStyle = computed(() => (isInline.value ? { width: resolvedLabelWidth.value } : undefined))\n\n/**\n * Пустое значение печатается прочерком, а не пропускается: «поле есть, значения\n * нет» и «поля нет» — разные утверждения, и выпавшая строка ломает и\n * выравнивание, и чтение.\n */\nfunction isEmptyValue(item: GrDescriptionItem): boolean {\n return item.value === null || item.value === undefined || item.value === ''\n}\n\nfunction valueClass(item: GrDescriptionItem): string {\n return [\n descriptionValueClass,\n item.tone ? descriptionValueToneClass[item.tone] : '',\n ].filter(Boolean).join(' ')\n}\n\nfunction pairKey(item: GrDescriptionItem, index: number): string {\n return item.name ?? `${item.label}-${index}`\n}\n\nfunction hasSlot(name: string): boolean {\n return Boolean(slots[name])\n}\n</script>\n\n<template>\n <dl ref=\"rootEl\" data-gr-description-list :class=\"rootClass\">\n <!-- Пара оборачивается в `<div>`: `dl > div > dt + dd` валиден по HTML5 и\n нужен для раскладки. Голые `<div>` вместо `dt`/`dd` — то, ради чего\n компонент и заводится. -->\n <div\n v-for=\"(item, index) in items\"\n :key=\"pairKey(item, index)\"\n data-gr-description-pair\n :class=\"pairClass\"\n >\n <dt data-gr-description-label :class=\"labelClass\" :style=\"labelStyle\">\n <slot v-if=\"item.name && hasSlot(`label-${item.name}`)\" :name=\"`label-${item.name}`\" :item=\"item\" />\n <template v-else>\n {{ item.label }}\n </template>\n </dt>\n\n <dd data-gr-description-value :class=\"valueClass(item)\">\n <slot v-if=\"item.name && hasSlot(`value-${item.name}`)\" :name=\"`value-${item.name}`\" :item=\"item\" />\n <template v-else-if=\"isEmptyValue(item)\">\n <slot name=\"empty\" :item=\"item\">\n {{ resolvedEmptyText }}\n </slot>\n </template>\n <template v-else>\n {{ item.value }}\n </template>\n </dd>\n </div>\n </dl>\n</template>\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrLoading-C3DZSSuh.js","names":[],"sources":["../../../../src/components/GrLoading/GrLoading.vue","../../../../src/components/GrLoading/GrLoading.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, type Component } from 'vue'\n\nimport IconLoader from '~icons/lucide/loader-circle'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport type { GrIconSize, GrIconTone } from '../GrIcon/grIconStyles'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { grLoadingRootClass } from './grLoadingStyles'\n\n/**\n * Пропы {@link GrLoading}.\n *\n * Оверлей загрузки: спиннер с подписью поверх соседнего контента. `fullscreen`\n * накрывает весь экран, иначе оверлей ложится на ближайшего позиционированного\n * предка.\n */\nexport interface GrLoadingProps {\n /** Подпись под спиннером. Пустая строка убирает её совсем. По умолчанию — из локали. */\n text?: string\n /** Свой компонент спиннера вместо иконки по умолчанию. */\n spinner?: Component\n /** Дополнительные классы обёртки спиннера. */\n spinnerClass?: string\n /** Размер спиннера: шкала пакета либо произвольный в пикселях. */\n spinnerSize?: GrIconSize | number\n /** Тон спиннера из палитры. */\n spinnerTone?: GrIconTone\n /** Вращение спиннера. По умолчанию включено. */\n animated?: boolean\n /** Свой `background-color`. Задан — дефолтный скрим `--gr-overlay-bg` снимается. */\n background?: string\n /** Накрыть весь экран (`position: fixed`) вместо ближайшего позиционированного предка. */\n fullscreen?: boolean\n /** Имя CSS-переменной слоя — escape-hatch мимо `--gr-z-loading`. */\n zIndexVar?: string\n /** Задержка показа в миллисекундах: короткая загрузка не мигает оверлеем. */\n delay?: number\n /** Дополнительные классы корня оверлея. */\n customClass?: string\n}\n\nexport interface GrLoadingEmits {\n /**\n * Оверлей появился на экране — сразу или по истечении `delay`. По этому\n * событию директива блокирует контент под оверлеем: до показа блокировать\n * нечего, иначе быстрый запрос молча «замораживал» бы форму.\n */\n (e: 'show'): void\n}\n\nconst props = withDefaults(\n defineProps<GrLoadingProps>(),\n {\n text: undefined,\n spinner: undefined,\n spinnerClass: undefined,\n // 28px не входит в шкалу иконок (14–20): спиннер загрузки — самостоятельный\n // акцент панели, а не иконка рядом с текстом.\n spinnerSize: 28,\n spinnerTone: 'neutral',\n animated: true,\n background: undefined,\n fullscreen: false,\n zIndexVar: undefined,\n delay: 0,\n customClass: undefined,\n },\n)\n\nconst emit = defineEmits<GrLoadingEmits>()\n\ndefineSlots<{\n /** Содержимое панели целиком вместо спиннера с подписью. */\n default?: () => any\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst Spinner = computed(() => props.spinner ?? IconLoader)\n\nconst displayText = computed(() => props.text ?? t('gr.loading.defaultText', 'Loading...'))\n\nconst rootClass = computed(() => {\n return grLoadingRootClass({\n fullscreen: props.fullscreen,\n hasBackground: props.background !== undefined,\n customClass: props.customClass,\n })\n})\n\nconst rootStyle = computed(() => {\n return {\n backgroundColor: props.background,\n zIndex: props.zIndexVar ? `var(${props.zIndexVar})` : undefined,\n } as Record<string, string | undefined>\n})\n\n// Оверлей на быстром запросе успевает мигнуть и раздражает сильнее, чем его\n// отсутствие. Отсчёт начинается с монтирования: компонент создаётся ровно в\n// момент старта загрузки — и в `v-if`, и в директиве.\nconst visible = ref(props.delay <= 0)\nlet delayTimer: ReturnType<typeof setTimeout> | undefined\n\nonMounted(() => {\n if (visible.value) {\n emit('show')\n return\n }\n\n delayTimer = setTimeout(() => {\n visible.value = true\n emit('show')\n }, props.delay)\n})\n\nonBeforeUnmount(() => {\n clearTimeout(delayTimer)\n})\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n data-gr-loading\n class=\"flex items-center justify-center cursor-wait select-none pointer-events-auto\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <div\n data-gr-loading-panel\n class=\"flex flex-col items-center justify-center gap-2 text-center rounded-[var(--gr-radius-md)] bg-[var(--gr-bg)]/55 px-5 py-4 shadow-lg\"\n >\n <slot>\n <GrIcon\n data-gr-loading-spinner\n :size=\"spinnerSize\"\n :tone=\"spinnerTone\"\n :spin=\"animated\"\n :class=\"spinnerClass\"\n >\n <component :is=\"Spinner\" />\n </GrIcon>\n <div v-if=\"displayText\" data-gr-loading-text class=\"text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] text-[var(--gr-muted-fg)]\">\n {{ displayText }}\n </div>\n </slot>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, type Component } from 'vue'\n\nimport IconLoader from '~icons/lucide/loader-circle'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport type { GrIconSize, GrIconTone } from '../GrIcon/grIconStyles'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { grLoadingRootClass } from './grLoadingStyles'\n\n/**\n * Пропы {@link GrLoading}.\n *\n * Оверлей загрузки: спиннер с подписью поверх соседнего контента. `fullscreen`\n * накрывает весь экран, иначе оверлей ложится на ближайшего позиционированного\n * предка.\n */\nexport interface GrLoadingProps {\n /** Подпись под спиннером. Пустая строка убирает её совсем. По умолчанию — из локали. */\n text?: string\n /** Свой компонент спиннера вместо иконки по умолчанию. */\n spinner?: Component\n /** Дополнительные классы обёртки спиннера. */\n spinnerClass?: string\n /** Размер спиннера: шкала пакета либо произвольный в пикселях. */\n spinnerSize?: GrIconSize | number\n /** Тон спиннера из палитры. */\n spinnerTone?: GrIconTone\n /** Вращение спиннера. По умолчанию включено. */\n animated?: boolean\n /** Свой `background-color`. Задан — дефолтный скрим `--gr-overlay-bg` снимается. */\n background?: string\n /** Накрыть весь экран (`position: fixed`) вместо ближайшего позиционированного предка. */\n fullscreen?: boolean\n /** Имя CSS-переменной слоя — escape-hatch мимо `--gr-z-loading`. */\n zIndexVar?: string\n /** Задержка показа в миллисекундах: короткая загрузка не мигает оверлеем. */\n delay?: number\n /** Дополнительные классы корня оверлея. */\n customClass?: string\n}\n\nexport interface GrLoadingEmits {\n /**\n * Оверлей появился на экране — сразу или по истечении `delay`. По этому\n * событию директива блокирует контент под оверлеем: до показа блокировать\n * нечего, иначе быстрый запрос молча «замораживал» бы форму.\n */\n (e: 'show'): void\n}\n\nconst props = withDefaults(\n defineProps<GrLoadingProps>(),\n {\n text: undefined,\n spinner: undefined,\n spinnerClass: undefined,\n // 28px не входит в шкалу иконок (14–20): спиннер загрузки — самостоятельный\n // акцент панели, а не иконка рядом с текстом.\n spinnerSize: 28,\n spinnerTone: 'neutral',\n animated: true,\n background: undefined,\n fullscreen: false,\n zIndexVar: undefined,\n delay: 0,\n customClass: undefined,\n },\n)\n\nconst emit = defineEmits<GrLoadingEmits>()\n\ndefineSlots<{\n /** Содержимое панели целиком вместо спиннера с подписью. */\n default?: () => any\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst Spinner = computed(() => props.spinner ?? IconLoader)\n\nconst displayText = computed(() => props.text ?? t('gr.loading.defaultText', 'Loading...'))\n\nconst rootClass = computed(() => {\n return grLoadingRootClass({\n fullscreen: props.fullscreen,\n hasBackground: props.background !== undefined,\n customClass: props.customClass,\n })\n})\n\nconst rootStyle = computed(() => {\n return {\n backgroundColor: props.background,\n zIndex: props.zIndexVar ? `var(${props.zIndexVar})` : undefined,\n } as Record<string, string | undefined>\n})\n\n// Оверлей на быстром запросе успевает мигнуть и раздражает сильнее, чем его\n// отсутствие. Отсчёт начинается с монтирования: компонент создаётся ровно в\n// момент старта загрузки — и в `v-if`, и в директиве.\nconst visible = ref(props.delay <= 0)\nlet delayTimer: ReturnType<typeof setTimeout> | undefined\n\nonMounted(() => {\n if (visible.value) {\n emit('show')\n return\n }\n\n delayTimer = setTimeout(() => {\n visible.value = true\n emit('show')\n }, props.delay)\n})\n\nonBeforeUnmount(() => {\n clearTimeout(delayTimer)\n})\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n data-gr-loading\n class=\"flex items-center justify-center cursor-wait select-none pointer-events-auto\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <div\n data-gr-loading-panel\n class=\"flex flex-col items-center justify-center gap-2 text-center rounded-[var(--gr-radius-md)] bg-[var(--gr-bg)]/55 px-5 py-4 shadow-lg\"\n >\n <slot>\n <GrIcon\n data-gr-loading-spinner\n :size=\"spinnerSize\"\n :tone=\"spinnerTone\"\n :spin=\"animated\"\n :class=\"spinnerClass\"\n >\n <component :is=\"Spinner\" />\n </GrIcon>\n <div v-if=\"displayText\" data-gr-loading-text class=\"text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] text-[var(--gr-muted-fg)]\">\n {{ displayText }}\n </div>\n </slot>\n </div>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmDA,MAAM,QAAQ;EAmBd,MAAM,OAAO;EAOb,MAAM,EAAE,MAAM,2BAA2B;EAEzC,MAAM,UAAU,eAAe,MAAM,WAAW,qBAAU;EAE1D,MAAM,cAAc,eAAe,MAAM,QAAQ,EAAE,0BAA0B,YAAY,CAAC;EAE1F,MAAM,YAAY,eAAe;GAC/B,OAAO,mBAAmB;IACxB,YAAY,MAAM;IAClB,eAAe,MAAM,eAAe,KAAA;IACpC,aAAa,MAAM;GACrB,CAAC;EACH,CAAC;EAED,MAAM,YAAY,eAAe;GAC/B,OAAO;IACL,iBAAiB,MAAM;IACvB,QAAQ,MAAM,YAAY,OAAO,MAAM,UAAU,KAAK,KAAA;GACxD;EACF,CAAC;EAKD,MAAM,UAAU,IAAI,MAAM,SAAS,CAAC;EACpC,IAAI;EAEJ,gBAAgB;GACd,IAAI,QAAQ,OAAO;IACjB,KAAK,MAAM;IACX;GACF;GAEA,aAAa,iBAAiB;IAC5B,QAAQ,QAAQ;IAChB,KAAK,MAAM;GACb,GAAG,MAAM,KAAK;EAChB,CAAC;EAED,sBAAsB;GACpB,aAAa,UAAU;EACzB,CAAC;;UAKS,QAAA,SAAA,UAAA,GADR,mBA4BM,OAAA;;IA1BJ,mBAAA;IACA,OAAK,eAAA,CAAC,gFACE,UAAA,KAAS,CAAA;IAChB,OAAK,eAAE,UAAA,KAAS;IACjB,MAAK;IACL,aAAU;OAEV,mBAkBM,OAlBN,YAkBM,CAdJ,WAaO,KAAA,QAAA,WAAA,CAAA,SAAA,CAZL,YAQS,gBAAA;IAPP,2BAAA;IACC,MAAM,QAAA;IACN,MAAM,QAAA;IACN,MAAM,QAAA;IACN,OAAK,eAAE,QAAA,YAAY;;2BAEO,EAAA,UAAA,GAA3B,YAA2B,wBAAX,QAAA,KAAO,CAAA,EAAA,CAAA;;;;;;;OAEd,YAAA,SAAA,UAAA,GAAX,mBAEM,OAFN,YAEM,gBADD,YAAA,KAAW,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrSegmented-D1mjxF_O.js","names":[],"sources":["../../../../src/components/GrSegmented/GrSegmented.vue","../../../../src/components/GrSegmented/GrSegmented.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\n\nimport type { ComponentPublicInstance } from 'vue'\nimport IconLoader from '~icons/lucide/loader-circle'\n\nimport {\n grSegmentedIndicatorClass,\n grSegmentedItemClass,\n grSegmentedItemIconClass,\n grSegmentedItemLabelClass,\n grSegmentedItemSpinnerClass,\n grSegmentedRootClass,\n grSegmentedRootStyle,\n type GrSegmentedOption,\n type GrSegmentedOrientation,\n type GrSegmentedSize,\n type GrSegmentedValue,\n type GrSegmentedVariant,\n} from './grSegmentedStyles'\n\ntype IndicatorGeometry = {\n width: number\n height: number\n x: number\n y: number\n}\n\n/**\n * Пропсы публичного GR-примитива «Segmented».\n */\nexport interface GrSegmentedProps {\n modelValue: GrSegmentedValue\n options: GrSegmentedOption[]\n variant?: GrSegmentedVariant\n size?: GrSegmentedSize\n /**\n * Направление ряда. Вертикаль — боковые фильтры; индикатор к ней готов по\n * построению, он двумерный.\n */\n orientation?: GrSegmentedOrientation\n /** Длительность анимации индикатора в мс. */\n indicatorDuration?: number\n /** Растягивать сегмент на всю ширину контейнера. */\n block?: boolean\n disabled?: boolean\n /** Только для чтения: выбор видно, но он не меняется. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n /** Имя скрытого поля, которым выбранное значение уходит в нативную форму. */\n name?: string\n ariaLabel?: string\n}\n\nexport interface GrSegmentedEmits {\n (e: 'update:modelValue', value: GrSegmentedValue): void\n (e: 'change', value: GrSegmentedValue, option: GrSegmentedOption): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrSegmentedProps>(),\n {\n variant: undefined,\n size: undefined,\n orientation: 'horizontal',\n indicatorDuration: 300,\n block: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n name: undefined,\n ariaLabel: undefined,\n },\n)\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrSegmented' })\nconst resolvedVariant = useGrComponentProp('GrSegmented', 'variant', () => props.variant, 'pills')\n\nconst emit = defineEmits<GrSegmentedEmits>()\ndefineSlots<{\n /** Содержимое сегмента вместо подписи из `options`. */\n default?: (props: {\n option: GrSegmentedOption\n selected: boolean\n disabled: boolean\n loading: boolean\n }) => any\n}>()\n\n\n// `useId()` стабилен между сервером и клиентом, в отличие от `instance.uid`.\nconst fallbackName = `gr-segmented-${useId()}`\n\n// Контекст `GrFormField` + общий контракт форм-контрола.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst labelledBy = computed(() => (props.ariaLabel ? undefined : field?.labelId.value))\nconst {\n disabled: isDisabled,\n invalid: isInvalid, required: isRequired, readonly: isReadonly } = useGrFormControl(() => props)\n\nconst rootRef = ref<HTMLElement | null>(null)\n\n// Фокус ходит между сегментами: без границы каждая стрелка давала бы\n// потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootRef, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n\ndefineExpose({ focus, blur })\nconst itemRefs = ref(new Map<string, HTMLElement>())\nconst indicatorGeometry = ref<IndicatorGeometry | null>(null)\nconst indicatorReady = ref(false)\n\nlet resizeObserver: ResizeObserver | null = null\nlet observedRoot: HTMLElement | null = null\nlet observedItem: HTMLElement | null = null\nlet scheduled = false\n\nconst selectedIndex = computed(() => props.options.findIndex(option => option.value === props.modelValue))\nconst selectedOption = computed(() => props.options[selectedIndex.value] ?? null)\nconst indicatorDuration = computed(() => Math.max(0, Math.round(props.indicatorDuration)))\nconst isVertical = computed(() => props.orientation === 'vertical')\nconst rootClassName = computed(() => grSegmentedRootClass({\n variant: resolvedVariant.value,\n orientation: props.orientation,\n block: props.block,\n disabled: isDisabled.value,\n}))\n\n/**\n * Раскладка задаётся треками, а не классами: число сегментов известно только в\n * рантайме. По вертикали колонка одна — сегменты одинаковой ширины по\n * построению, и `block` решает лишь, занимать ли ширину контейнера.\n */\nconst rootStyle = computed<Record<string, string>>(() => {\n const empty = props.options.length === 0\n const track = () => props.block && !isVertical.value ? 'minmax(0,1fr)' : 'minmax(0,max-content)'\n\n return {\n ...grSegmentedRootStyle({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n orientation: props.orientation,\n }),\n gridTemplateColumns: isVertical.value\n ? 'minmax(0,1fr)'\n : empty ? 'none' : props.options.map(track).join(' '),\n ...(isVertical.value\n ? { gridTemplateRows: empty ? 'none' : props.options.map(track).join(' ') }\n : {}),\n }\n})\nconst indicatorClassName = computed(() => grSegmentedIndicatorClass(resolvedVariant.value))\nconst resolvedName = computed(() => props.name ?? fallbackName)\nconst hasIndicator = computed(() => selectedIndex.value !== -1 && indicatorGeometry.value !== null)\n\nconst indicatorStyle = computed<Record<string, string>>(() => {\n const geometry = indicatorGeometry.value\n if (!geometry) {\n return {\n opacity: '0',\n width: '0px',\n height: '0px',\n transitionDuration: `${indicatorDuration.value}ms`,\n transform: 'translate3d(0px, 0px, 0px)',\n }\n }\n\n return {\n opacity: indicatorReady.value ? '1' : '0',\n width: `${geometry.width}px`,\n height: `${geometry.height}px`,\n transitionDuration: `${indicatorDuration.value}ms`,\n transform: `translate3d(${geometry.x}px, ${geometry.y}px, 0)`,\n }\n})\n\nfunction getOptionKey(value: GrSegmentedValue): string {\n return String(value)\n}\n\nfunction resolveOptionDisabled(option: GrSegmentedOption): boolean {\n return isDisabled.value || Boolean(option.disabled)\n}\n\n/**\n * Занятый сегмент не «недоступен», а работает: нативный `disabled` и\n * `aria-disabled` ему не ставим — состояние несёт `aria-busy`. Но выбор он не\n * принимает, поэтому блокировка считается отдельно от `disabled`.\n */\nfunction isOptionBusy(option: GrSegmentedOption): boolean {\n return Boolean(option.loading)\n}\n\nfunction isOptionBlocked(option: GrSegmentedOption): boolean {\n return resolveOptionDisabled(option) || isOptionBusy(option)\n}\n\nfunction isOptionSelected(option: GrSegmentedOption): boolean {\n return option.value === props.modelValue\n}\n\nfunction isIconOnlyOption(option: GrSegmentedOption): boolean {\n return Boolean(option.icon) && !option.label\n}\n\nfunction setItemRef(value: GrSegmentedValue, element: Element | ComponentPublicInstance | null): void {\n const key = getOptionKey(value)\n\n if (!(element instanceof HTMLElement)) {\n itemRefs.value.delete(key)\n return\n }\n\n itemRefs.value.set(key, element)\n}\n\nfunction isSameGeometry(left: IndicatorGeometry | null, right: IndicatorGeometry | null): boolean {\n if (left === right) {\n return true\n }\n\n if (!left || !right) {\n return false\n }\n\n return left.width === right.width\n && left.height === right.height\n && left.x === right.x\n && left.y === right.y\n}\n\nfunction measureIndicator(): void {\n const root = rootRef.value\n const option = selectedOption.value\n if (!root || !option) {\n if (indicatorGeometry.value !== null)\n indicatorGeometry.value = null\n\n if (indicatorReady.value)\n indicatorReady.value = false\n\n return\n }\n\n const selectedItem = itemRefs.value.get(getOptionKey(option.value))\n if (!selectedItem) {\n return\n }\n\n const rootRect = root.getBoundingClientRect()\n const itemRect = selectedItem.getBoundingClientRect()\n\n const nextGeometry = {\n width: Math.max(Math.round(itemRect.width), 0),\n height: Math.max(Math.round(itemRect.height), 0),\n x: Math.round(itemRect.left - rootRect.left),\n y: Math.max(Math.round(itemRect.top - rootRect.top) - 1, 0),\n } satisfies IndicatorGeometry\n\n if (!isSameGeometry(indicatorGeometry.value, nextGeometry)) {\n indicatorGeometry.value = nextGeometry\n }\n\n const nextReady = nextGeometry.width > 0 && nextGeometry.height > 0\n if (indicatorReady.value !== nextReady) {\n indicatorReady.value = nextReady\n }\n}\n\nfunction refreshObserver(): void {\n if (typeof ResizeObserver === 'undefined') {\n return\n }\n\n const root = rootRef.value\n const option = selectedOption.value\n const selectedItem = option\n ? itemRefs.value.get(getOptionKey(option.value)) ?? null\n : null\n\n if (!resizeObserver) {\n resizeObserver = new ResizeObserver(() => scheduleMeasure())\n }\n\n if (!root) {\n resizeObserver.disconnect()\n observedRoot = null\n observedItem = null\n return\n }\n\n if (observedRoot === root && observedItem === selectedItem) {\n return\n }\n\n resizeObserver.disconnect()\n resizeObserver.observe(root)\n\n if (selectedItem) {\n resizeObserver.observe(selectedItem)\n }\n\n observedRoot = root\n observedItem = selectedItem\n}\n\nfunction scheduleMeasure(): void {\n if (scheduled) {\n return\n }\n\n scheduled = true\n void nextTick(() => {\n scheduled = false\n measureIndicator()\n refreshObserver()\n })\n}\n\nfunction emitValue(option: GrSegmentedOption): void {\n // Один guard на все пути: проверяй `readonly` только клавиатура, и клик по\n // сегменту менял бы значение вопреки заявленному контракту.\n if (isReadonly.value || isOptionBlocked(option) || option.value === props.modelValue) {\n return\n }\n\n emit('update:modelValue', option.value)\n emit('change', option.value, option)\n}\n\nfunction optionOf(value: GrSegmentedValue): GrSegmentedOption | undefined {\n return props.options.find(option => option.value === value)\n}\n\n/**\n * Кольцо roving-фокуса. Обе оси работают всегда: `orientation` у сегмента\n * описывает раскладку и уходит в `aria-orientation`, а не сужает клавиатуру —\n * вертикальный набор листается и стрелками влево/вправо.\n */\nconst roving = useRovingFocus<GrSegmentedValue>({\n items: () => props.options.map(option => option.value),\n elementFor: value => itemRefs.value.get(getOptionKey(value)),\n isDisabled: (value) => {\n const option = optionOf(value)\n return option ? isOptionBlocked(option) : true\n },\n orientation: () => 'both',\n skipDisabled: () => true,\n /**\n * Выбранный сегмент держит остановку `Tab` — кроме выключенного: на нём\n * стоит нативный `disabled`, и фокус на него не встанет вовсе. Загружающийся\n * остановку сохраняет: он не «недоступен», а занят.\n */\n initialKey: () => {\n const selected = selectedOption.value\n return selected && !resolveOptionDisabled(selected) ? selected.value : undefined\n },\n // Стрелка в `radiogroup` переносит и выбор, а не только фокус.\n onMove: (value) => {\n const option = optionOf(value)\n if (option) emitValue(option)\n },\n})\n\nfunction onItemClick(option: GrSegmentedOption): void {\n emitValue(option)\n // Клик тоже переносит остановку `Tab`: иначе она осталась бы там, куда её\n // увела последняя стрелка.\n if (!isOptionBlocked(option)) roving.setActive(option.value)\n}\n\n/** Императивный фокус ведёт на текущую остановку `Tab`, а не на первый сегмент. */\nfunction rovingElement(): HTMLElement | undefined {\n const key = roving.rovingKey.value\n return key === undefined ? undefined : itemRefs.value.get(getOptionKey(key))\n}\n\nfunction focus(): void {\n rovingElement()?.focus()\n}\n\nfunction blur(): void {\n rovingElement()?.blur()\n}\n\nfunction onKeydown(event: KeyboardEvent, index: number): void {\n if (isDisabled.value || isReadonly.value) {\n return\n }\n\n if (roving.handleNavigationKeys(event)) {\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault()\n const option = props.options[index]\n if (option) {\n emitValue(option)\n }\n }\n}\n\nwatch(() => props.modelValue, () => scheduleMeasure())\nwatch(() => props.options, () => scheduleMeasure(), { deep: true })\nwatch(resolvedVariant, () => scheduleMeasure())\nwatch(resolvedSize, () => scheduleMeasure())\nwatch(() => props.block, () => scheduleMeasure())\n// Смена ориентации переставляет сегменты: без пересчёта индикатор остался бы в\n// координатах прежней раскладки.\nwatch(() => props.orientation, () => scheduleMeasure())\n\nonMounted(() => scheduleMeasure())\nonBeforeUnmount(() => {\n resizeObserver?.disconnect()\n observedRoot = null\n observedItem = null\n})\n</script>\n\n<template>\n <div\n :id=\"fieldId\"\n ref=\"rootRef\"\n data-gr-segmented\n :data-variant=\"resolvedVariant\"\n role=\"radiogroup\"\n :aria-orientation=\"orientation\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n :aria-describedby=\"describedBy\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-disabled=\"isDisabled ? 'true' : undefined\"\n :class=\"rootClassName\"\n :style=\"rootStyle\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <!-- Значение для нативной формы уходит одним скрытым полем рядом с\n сегментами, а не вложенным в каждый `role=\"radio\"`: роль объявляет своих\n потомков презентационными, и вложенный интерактивный контрол ломает\n виджет для скринридеров (axe: `nested-interactive`). Скрытое поле не\n фокусируется и интерактивным не считается. -->\n <input\n v-if=\"selectedOption && !resolveOptionDisabled(selectedOption)\"\n type=\"hidden\"\n :name=\"resolvedName\"\n :value=\"String(selectedOption.value)\"\n >\n\n <span\n v-if=\"hasIndicator\"\n data-gr-segmented-indicator\n aria-hidden=\"true\"\n :class=\"indicatorClassName\"\n :style=\"indicatorStyle\"\n />\n\n <button\n v-for=\"(option, index) in options\"\n :key=\"option.value\"\n :ref=\"element => setItemRef(option.value, element)\"\n data-gr-segmented-item\n :data-value=\"String(option.value)\"\n type=\"button\"\n role=\"radio\"\n :aria-label=\"option.ariaLabel\"\n :aria-checked=\"isOptionSelected(option) ? 'true' : 'false'\"\n :aria-disabled=\"resolveOptionDisabled(option) ? 'true' : undefined\"\n :aria-busy=\"isOptionBusy(option) ? 'true' : undefined\"\n :disabled=\"resolveOptionDisabled(option)\"\n :tabindex=\"roving.tabindexFor(option.value)\"\n :class=\"grSegmentedItemClass({\n variant: resolvedVariant,\n selected: isOptionSelected(option),\n disabled: resolveOptionDisabled(option),\n iconOnly: isIconOnlyOption(option),\n })\"\n @click=\"onItemClick(option)\"\n @keydown=\"onKeydown($event, index)\"\n >\n <slot\n :option=\"option\"\n :selected=\"isOptionSelected(option)\"\n :disabled=\"resolveOptionDisabled(option)\"\n :loading=\"isOptionBusy(option)\"\n >\n <!-- Спиннер занимает место иконки: две крутилки рядом читались бы как ошибка. -->\n <IconLoader\n v-if=\"isOptionBusy(option)\"\n data-gr-segmented-spinner\n aria-hidden=\"true\"\n :class=\"grSegmentedItemSpinnerClass\"\n />\n <component\n :is=\"option.icon\"\n v-else-if=\"option.icon\"\n aria-hidden=\"true\"\n :class=\"grSegmentedItemIconClass\"\n />\n <span v-if=\"option.label\" :class=\"grSegmentedItemLabelClass\">\n {{ option.label }}\n </span>\n </slot>\n </button>\n </div>\n</template>","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\n\nimport type { ComponentPublicInstance } from 'vue'\nimport IconLoader from '~icons/lucide/loader-circle'\n\nimport {\n grSegmentedIndicatorClass,\n grSegmentedItemClass,\n grSegmentedItemIconClass,\n grSegmentedItemLabelClass,\n grSegmentedItemSpinnerClass,\n grSegmentedRootClass,\n grSegmentedRootStyle,\n type GrSegmentedOption,\n type GrSegmentedOrientation,\n type GrSegmentedSize,\n type GrSegmentedValue,\n type GrSegmentedVariant,\n} from './grSegmentedStyles'\n\ntype IndicatorGeometry = {\n width: number\n height: number\n x: number\n y: number\n}\n\n/**\n * Пропсы публичного GR-примитива «Segmented».\n */\nexport interface GrSegmentedProps {\n modelValue: GrSegmentedValue\n options: GrSegmentedOption[]\n variant?: GrSegmentedVariant\n size?: GrSegmentedSize\n /**\n * Направление ряда. Вертикаль — боковые фильтры; индикатор к ней готов по\n * построению, он двумерный.\n */\n orientation?: GrSegmentedOrientation\n /** Длительность анимации индикатора в мс. */\n indicatorDuration?: number\n /** Растягивать сегмент на всю ширину контейнера. */\n block?: boolean\n disabled?: boolean\n /** Только для чтения: выбор видно, но он не меняется. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n /** Имя скрытого поля, которым выбранное значение уходит в нативную форму. */\n name?: string\n ariaLabel?: string\n}\n\nexport interface GrSegmentedEmits {\n (e: 'update:modelValue', value: GrSegmentedValue): void\n (e: 'change', value: GrSegmentedValue, option: GrSegmentedOption): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrSegmentedProps>(),\n {\n variant: undefined,\n size: undefined,\n orientation: 'horizontal',\n indicatorDuration: 300,\n block: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n name: undefined,\n ariaLabel: undefined,\n },\n)\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrSegmented' })\nconst resolvedVariant = useGrComponentProp('GrSegmented', 'variant', () => props.variant, 'pills')\n\nconst emit = defineEmits<GrSegmentedEmits>()\ndefineSlots<{\n /** Содержимое сегмента вместо подписи из `options`. */\n default?: (props: {\n option: GrSegmentedOption\n selected: boolean\n disabled: boolean\n loading: boolean\n }) => any\n}>()\n\n\n// `useId()` стабилен между сервером и клиентом, в отличие от `instance.uid`.\nconst fallbackName = `gr-segmented-${useId()}`\n\n// Контекст `GrFormField` + общий контракт форм-контрола.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst labelledBy = computed(() => (props.ariaLabel ? undefined : field?.labelId.value))\nconst {\n disabled: isDisabled,\n invalid: isInvalid, required: isRequired, readonly: isReadonly } = useGrFormControl(() => props)\n\nconst rootRef = ref<HTMLElement | null>(null)\n\n// Фокус ходит между сегментами: без границы каждая стрелка давала бы\n// потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootRef, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n\ndefineExpose({ focus, blur })\nconst itemRefs = ref(new Map<string, HTMLElement>())\nconst indicatorGeometry = ref<IndicatorGeometry | null>(null)\nconst indicatorReady = ref(false)\n\nlet resizeObserver: ResizeObserver | null = null\nlet observedRoot: HTMLElement | null = null\nlet observedItem: HTMLElement | null = null\nlet scheduled = false\n\nconst selectedIndex = computed(() => props.options.findIndex(option => option.value === props.modelValue))\nconst selectedOption = computed(() => props.options[selectedIndex.value] ?? null)\nconst indicatorDuration = computed(() => Math.max(0, Math.round(props.indicatorDuration)))\nconst isVertical = computed(() => props.orientation === 'vertical')\nconst rootClassName = computed(() => grSegmentedRootClass({\n variant: resolvedVariant.value,\n orientation: props.orientation,\n block: props.block,\n disabled: isDisabled.value,\n}))\n\n/**\n * Раскладка задаётся треками, а не классами: число сегментов известно только в\n * рантайме. По вертикали колонка одна — сегменты одинаковой ширины по\n * построению, и `block` решает лишь, занимать ли ширину контейнера.\n */\nconst rootStyle = computed<Record<string, string>>(() => {\n const empty = props.options.length === 0\n const track = () => props.block && !isVertical.value ? 'minmax(0,1fr)' : 'minmax(0,max-content)'\n\n return {\n ...grSegmentedRootStyle({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n orientation: props.orientation,\n }),\n gridTemplateColumns: isVertical.value\n ? 'minmax(0,1fr)'\n : empty ? 'none' : props.options.map(track).join(' '),\n ...(isVertical.value\n ? { gridTemplateRows: empty ? 'none' : props.options.map(track).join(' ') }\n : {}),\n }\n})\nconst indicatorClassName = computed(() => grSegmentedIndicatorClass(resolvedVariant.value))\nconst resolvedName = computed(() => props.name ?? fallbackName)\nconst hasIndicator = computed(() => selectedIndex.value !== -1 && indicatorGeometry.value !== null)\n\nconst indicatorStyle = computed<Record<string, string>>(() => {\n const geometry = indicatorGeometry.value\n if (!geometry) {\n return {\n opacity: '0',\n width: '0px',\n height: '0px',\n transitionDuration: `${indicatorDuration.value}ms`,\n transform: 'translate3d(0px, 0px, 0px)',\n }\n }\n\n return {\n opacity: indicatorReady.value ? '1' : '0',\n width: `${geometry.width}px`,\n height: `${geometry.height}px`,\n transitionDuration: `${indicatorDuration.value}ms`,\n transform: `translate3d(${geometry.x}px, ${geometry.y}px, 0)`,\n }\n})\n\nfunction getOptionKey(value: GrSegmentedValue): string {\n return String(value)\n}\n\nfunction resolveOptionDisabled(option: GrSegmentedOption): boolean {\n return isDisabled.value || Boolean(option.disabled)\n}\n\n/**\n * Занятый сегмент не «недоступен», а работает: нативный `disabled` и\n * `aria-disabled` ему не ставим — состояние несёт `aria-busy`. Но выбор он не\n * принимает, поэтому блокировка считается отдельно от `disabled`.\n */\nfunction isOptionBusy(option: GrSegmentedOption): boolean {\n return Boolean(option.loading)\n}\n\nfunction isOptionBlocked(option: GrSegmentedOption): boolean {\n return resolveOptionDisabled(option) || isOptionBusy(option)\n}\n\nfunction isOptionSelected(option: GrSegmentedOption): boolean {\n return option.value === props.modelValue\n}\n\nfunction isIconOnlyOption(option: GrSegmentedOption): boolean {\n return Boolean(option.icon) && !option.label\n}\n\nfunction setItemRef(value: GrSegmentedValue, element: Element | ComponentPublicInstance | null): void {\n const key = getOptionKey(value)\n\n if (!(element instanceof HTMLElement)) {\n itemRefs.value.delete(key)\n return\n }\n\n itemRefs.value.set(key, element)\n}\n\nfunction isSameGeometry(left: IndicatorGeometry | null, right: IndicatorGeometry | null): boolean {\n if (left === right) {\n return true\n }\n\n if (!left || !right) {\n return false\n }\n\n return left.width === right.width\n && left.height === right.height\n && left.x === right.x\n && left.y === right.y\n}\n\nfunction measureIndicator(): void {\n const root = rootRef.value\n const option = selectedOption.value\n if (!root || !option) {\n if (indicatorGeometry.value !== null)\n indicatorGeometry.value = null\n\n if (indicatorReady.value)\n indicatorReady.value = false\n\n return\n }\n\n const selectedItem = itemRefs.value.get(getOptionKey(option.value))\n if (!selectedItem) {\n return\n }\n\n const rootRect = root.getBoundingClientRect()\n const itemRect = selectedItem.getBoundingClientRect()\n\n const nextGeometry = {\n width: Math.max(Math.round(itemRect.width), 0),\n height: Math.max(Math.round(itemRect.height), 0),\n x: Math.round(itemRect.left - rootRect.left),\n y: Math.max(Math.round(itemRect.top - rootRect.top) - 1, 0),\n } satisfies IndicatorGeometry\n\n if (!isSameGeometry(indicatorGeometry.value, nextGeometry)) {\n indicatorGeometry.value = nextGeometry\n }\n\n const nextReady = nextGeometry.width > 0 && nextGeometry.height > 0\n if (indicatorReady.value !== nextReady) {\n indicatorReady.value = nextReady\n }\n}\n\nfunction refreshObserver(): void {\n if (typeof ResizeObserver === 'undefined') {\n return\n }\n\n const root = rootRef.value\n const option = selectedOption.value\n const selectedItem = option\n ? itemRefs.value.get(getOptionKey(option.value)) ?? null\n : null\n\n if (!resizeObserver) {\n resizeObserver = new ResizeObserver(() => scheduleMeasure())\n }\n\n if (!root) {\n resizeObserver.disconnect()\n observedRoot = null\n observedItem = null\n return\n }\n\n if (observedRoot === root && observedItem === selectedItem) {\n return\n }\n\n resizeObserver.disconnect()\n resizeObserver.observe(root)\n\n if (selectedItem) {\n resizeObserver.observe(selectedItem)\n }\n\n observedRoot = root\n observedItem = selectedItem\n}\n\nfunction scheduleMeasure(): void {\n if (scheduled) {\n return\n }\n\n scheduled = true\n void nextTick(() => {\n scheduled = false\n measureIndicator()\n refreshObserver()\n })\n}\n\nfunction emitValue(option: GrSegmentedOption): void {\n // Один guard на все пути: проверяй `readonly` только клавиатура, и клик по\n // сегменту менял бы значение вопреки заявленному контракту.\n if (isReadonly.value || isOptionBlocked(option) || option.value === props.modelValue) {\n return\n }\n\n emit('update:modelValue', option.value)\n emit('change', option.value, option)\n}\n\nfunction optionOf(value: GrSegmentedValue): GrSegmentedOption | undefined {\n return props.options.find(option => option.value === value)\n}\n\n/**\n * Кольцо roving-фокуса. Обе оси работают всегда: `orientation` у сегмента\n * описывает раскладку и уходит в `aria-orientation`, а не сужает клавиатуру —\n * вертикальный набор листается и стрелками влево/вправо.\n */\nconst roving = useRovingFocus<GrSegmentedValue>({\n items: () => props.options.map(option => option.value),\n elementFor: value => itemRefs.value.get(getOptionKey(value)),\n isDisabled: (value) => {\n const option = optionOf(value)\n return option ? isOptionBlocked(option) : true\n },\n orientation: () => 'both',\n skipDisabled: () => true,\n /**\n * Выбранный сегмент держит остановку `Tab` — кроме выключенного: на нём\n * стоит нативный `disabled`, и фокус на него не встанет вовсе. Загружающийся\n * остановку сохраняет: он не «недоступен», а занят.\n */\n initialKey: () => {\n const selected = selectedOption.value\n return selected && !resolveOptionDisabled(selected) ? selected.value : undefined\n },\n // Стрелка в `radiogroup` переносит и выбор, а не только фокус.\n onMove: (value) => {\n const option = optionOf(value)\n if (option) emitValue(option)\n },\n})\n\nfunction onItemClick(option: GrSegmentedOption): void {\n emitValue(option)\n // Клик тоже переносит остановку `Tab`: иначе она осталась бы там, куда её\n // увела последняя стрелка.\n if (!isOptionBlocked(option)) roving.setActive(option.value)\n}\n\n/** Императивный фокус ведёт на текущую остановку `Tab`, а не на первый сегмент. */\nfunction rovingElement(): HTMLElement | undefined {\n const key = roving.rovingKey.value\n return key === undefined ? undefined : itemRefs.value.get(getOptionKey(key))\n}\n\nfunction focus(): void {\n rovingElement()?.focus()\n}\n\nfunction blur(): void {\n rovingElement()?.blur()\n}\n\nfunction onKeydown(event: KeyboardEvent, index: number): void {\n if (isDisabled.value || isReadonly.value) {\n return\n }\n\n if (roving.handleNavigationKeys(event)) {\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault()\n const option = props.options[index]\n if (option) {\n emitValue(option)\n }\n }\n}\n\nwatch(() => props.modelValue, () => scheduleMeasure())\nwatch(() => props.options, () => scheduleMeasure(), { deep: true })\nwatch(resolvedVariant, () => scheduleMeasure())\nwatch(resolvedSize, () => scheduleMeasure())\nwatch(() => props.block, () => scheduleMeasure())\n// Смена ориентации переставляет сегменты: без пересчёта индикатор остался бы в\n// координатах прежней раскладки.\nwatch(() => props.orientation, () => scheduleMeasure())\n\nonMounted(() => scheduleMeasure())\nonBeforeUnmount(() => {\n resizeObserver?.disconnect()\n observedRoot = null\n observedItem = null\n})\n</script>\n\n<template>\n <div\n :id=\"fieldId\"\n ref=\"rootRef\"\n data-gr-segmented\n :data-variant=\"resolvedVariant\"\n role=\"radiogroup\"\n :aria-orientation=\"orientation\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n :aria-describedby=\"describedBy\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-disabled=\"isDisabled ? 'true' : undefined\"\n :class=\"rootClassName\"\n :style=\"rootStyle\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <!-- Значение для нативной формы уходит одним скрытым полем рядом с\n сегментами, а не вложенным в каждый `role=\"radio\"`: роль объявляет своих\n потомков презентационными, и вложенный интерактивный контрол ломает\n виджет для скринридеров (axe: `nested-interactive`). Скрытое поле не\n фокусируется и интерактивным не считается. -->\n <input\n v-if=\"selectedOption && !resolveOptionDisabled(selectedOption)\"\n type=\"hidden\"\n :name=\"resolvedName\"\n :value=\"String(selectedOption.value)\"\n >\n\n <span\n v-if=\"hasIndicator\"\n data-gr-segmented-indicator\n aria-hidden=\"true\"\n :class=\"indicatorClassName\"\n :style=\"indicatorStyle\"\n />\n\n <button\n v-for=\"(option, index) in options\"\n :key=\"option.value\"\n :ref=\"element => setItemRef(option.value, element)\"\n data-gr-segmented-item\n :data-value=\"String(option.value)\"\n type=\"button\"\n role=\"radio\"\n :aria-label=\"option.ariaLabel\"\n :aria-checked=\"isOptionSelected(option) ? 'true' : 'false'\"\n :aria-disabled=\"resolveOptionDisabled(option) ? 'true' : undefined\"\n :aria-busy=\"isOptionBusy(option) ? 'true' : undefined\"\n :disabled=\"resolveOptionDisabled(option)\"\n :tabindex=\"roving.tabindexFor(option.value)\"\n :class=\"grSegmentedItemClass({\n variant: resolvedVariant,\n selected: isOptionSelected(option),\n disabled: resolveOptionDisabled(option),\n iconOnly: isIconOnlyOption(option),\n })\"\n @click=\"onItemClick(option)\"\n @keydown=\"onKeydown($event, index)\"\n >\n <slot\n :option=\"option\"\n :selected=\"isOptionSelected(option)\"\n :disabled=\"resolveOptionDisabled(option)\"\n :loading=\"isOptionBusy(option)\"\n >\n <!-- Спиннер занимает место иконки: две крутилки рядом читались бы как ошибка. -->\n <IconLoader\n v-if=\"isOptionBusy(option)\"\n data-gr-segmented-spinner\n aria-hidden=\"true\"\n :class=\"grSegmentedItemSpinnerClass\"\n />\n <component\n :is=\"option.icon\"\n v-else-if=\"option.icon\"\n aria-hidden=\"true\"\n :class=\"grSegmentedItemIconClass\"\n />\n <span v-if=\"option.label\" :class=\"grSegmentedItemLabelClass\">\n {{ option.label }}\n </span>\n </slot>\n </button>\n </div>\n</template>"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrStatistic-C7wQECeS.js","names":["$slots"],"sources":["../../../../src/components/GrStatistic/countUp.ts","../../../../src/components/GrStatistic/formatStatisticValue.ts","../../../../src/components/GrStatistic/GrStatistic.vue","../../../../src/components/GrStatistic/GrStatistic.vue"],"sourcesContent":["/**\n * Перебор чисел для `GrStatistic`: чистая арифметика кадра без Vue и таймеров.\n *\n * Вынесено отдельным модулем по той же причине, что и `formatStatisticValue`:\n * кривая и округление проверяются напрямую, без монтирования и подмены `rAF`.\n */\n\n/** Быстрый старт, мягкая остановка — та же форма, что у `--gr-ease-out`. */\nexport function easeOutCubic(t: number): number {\n const clamped = Math.min(Math.max(t, 0), 1)\n return 1 - (1 - clamped) ** 3\n}\n\n/**\n * Значение кадра на `elapsed` миллисекунде перехода `from → to`.\n *\n * Округление до `precision` обязательно: без него кадры показывали бы\n * `1283.6666666666667` там, где показатель целый, и ширина строки прыгала бы\n * даже при `tabular-nums`. Последний кадр возвращает ровно `to` — приближение\n * не должно оставлять показатель в 1283.9999.\n */\nexport function countUpFrame(\n from: number,\n to: number,\n elapsed: number,\n duration: number,\n precision?: number,\n): number {\n if (!(duration > 0) || elapsed >= duration) return to\n\n const digits = Math.min(Math.max(precision ?? 0, 0), 20)\n const value = from + (to - from) * easeOutCubic(elapsed / duration)\n\n return Number(value.toFixed(digits))\n}\n","export type GrStatisticFormatOptions = {\n /** Число знаков после запятой. `undefined` — оставить как есть. */\n precision?: number\n /** Разделитель групп разрядов. Сильнее локали. */\n groupSeparator?: string\n /** Десятичный разделитель. Сильнее локали. */\n decimalSeparator?: string\n /** BCP-47 локаль: разделители берутся из `Intl.NumberFormat`. */\n locale?: string\n}\n\nconst DEFAULT_GROUP_SEPARATOR = ' '\nconst DEFAULT_DECIMAL_SEPARATOR = '.'\n\n/**\n * Форматирует значение показателя: фиксирует точность и расставляет разделители.\n *\n * Нечисловые значения возвращаются без изменений — это позволяет показывать в\n * `GrStatistic` уже готовые строки («2 ч 15 мин», «—») без обходных пропов.\n * Вынесено из SFC отдельным модулем, чтобы тестировалось без монтирования.\n *\n * Порядок разрешения разделителей: явный проп → локаль → встроенный дефолт.\n */\nexport function formatStatisticValue(\n value: number | string,\n options: GrStatisticFormatOptions = {},\n): string {\n const { precision, groupSeparator, decimalSeparator, locale } = options\n\n const numeric = typeof value === 'number' ? value : Number(value)\n if (typeof value === 'string' && (value.trim() === '' || !Number.isFinite(numeric))) {\n return value\n }\n if (!Number.isFinite(numeric)) return String(value)\n\n if (locale) {\n return formatByLocale(numeric, locale, { precision, groupSeparator, decimalSeparator })\n }\n\n const fixed = precision === undefined ? String(numeric) : numeric.toFixed(precision)\n // `toFixed` не даёт экспоненциальной записи, а `String(число)` — может (1e21+).\n // В таком случае группировать нечего: возвращаем как есть.\n if (fixed.includes('e') || fixed.includes('E')) return fixed\n\n const negative = fixed.startsWith('-')\n const [intPart, fracPart] = (negative ? fixed.slice(1) : fixed).split('.')\n\n const separator = groupSeparator ?? DEFAULT_GROUP_SEPARATOR\n const grouped = separator\n ? intPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, separator)\n : intPart\n\n return [\n negative ? '-' : '',\n grouped,\n fracPart ? `${decimalSeparator ?? DEFAULT_DECIMAL_SEPARATOR}${fracPart}` : '',\n ].join('')\n}\n\n/**\n * `formatToParts` вместо `format`: даёт заменить отдельные разделители, оставив\n * остальные правила локали (индийская группировка, знак минуса, порядок частей).\n */\nfunction formatByLocale(\n value: number,\n locale: string,\n options: Pick<GrStatisticFormatOptions, 'precision' | 'groupSeparator' | 'decimalSeparator'>,\n): string {\n const { precision, groupSeparator, decimalSeparator } = options\n // `Intl` принимает не больше 20 знаков и падает `RangeError` на большем,\n // тогда как ручная ветка через `toFixed` пережила бы до 100.\n const digits = precision === undefined ? undefined : Math.min(Math.max(precision, 0), 20)\n\n return new Intl.NumberFormat(locale, {\n useGrouping: true,\n minimumFractionDigits: digits,\n maximumFractionDigits: digits ?? 20,\n })\n .formatToParts(value)\n .map((part) => {\n if (part.type === 'group' && groupSeparator !== undefined) return groupSeparator\n if (part.type === 'decimal' && decimalSeparator !== undefined) return decimalSeparator\n return part.value\n })\n .join('')\n}\n","<script setup lang=\"ts\">\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { computed, markRaw, onBeforeUnmount, onMounted, ref, useSlots, watch, type Component } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { deltaTone, type GrDeltaPolarity } from '../GrDelta/deltaTone'\nimport { iconClass, iconTag } from '../shared/icon'\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\n\nimport IconMinus from '~icons/lucide/minus'\nimport IconTrendingDown from '~icons/lucide/trending-down'\nimport IconTrendingUp from '~icons/lucide/trending-up'\n\nimport { countUpFrame } from './countUp'\nimport { formatStatisticValue } from './formatStatisticValue'\n\nimport {\n statisticAffixSizeBySize,\n statisticPlaceholderHeightBySize,\n statisticRootClass,\n statisticTitleClass,\n statisticTitleSizeBySize,\n statisticTrendClassByTrend,\n statisticTrendSizeBySize,\n statisticValueClass,\n type GrStatisticSize,\n type GrStatisticTone,\n type GrStatisticTrend,\n} from './grStatisticStyles'\n\nexport type {\n GrStatisticSize,\n GrStatisticTone,\n GrStatisticTrend,\n} from './grStatisticStyles'\nexport type { GrStatisticFormatOptions } from './formatStatisticValue'\n\n/**\n * Публичный GR-примитив «Statistic» — крупный числовой показатель с подписью,\n * префиксом/суффиксом и строкой динамики.\n *\n * Компонент презентационный: не грузит данные и не считает дельту, только\n * форматирует и подаёт. Форматирование — чистая функция `formatStatisticValue`.\n */\nexport interface GrStatisticProps {\n /** Значение показателя. Нечисловая строка выводится как есть. */\n value: number | string\n /** Подпись над значением. */\n title?: string\n /** Число знаков после запятой. */\n precision?: number\n /** Разделитель разрядов. Сильнее локали; без него и без локали — узкий пробел. */\n groupSeparator?: string\n /** Десятичный разделитель. Сильнее локали; без него и без локали — точка. */\n decimalSeparator?: string\n /**\n * BCP-47 локаль форматирования. Не задана — берётся из i18n-адаптера; нет и\n * адаптера — работают ручные разделители.\n */\n locale?: string\n /** Приписка перед значением (валюта, знак). */\n prefix?: string\n /** Приписка после значения (единица измерения, `%`). */\n suffix?: string\n /**\n * Иконка слева от блока: Vue-компонент либо класс иконки вашей UnoCSS-сборки\n * (`'i-lucide-users'` — тогда нужен ваш `presetIcons`, см. `docs/installation.md`).\n */\n icon?: string | Component\n size?: GrStatisticSize\n /** Тон значения; точечно перекрывается `--gr-statistic-value-color`. */\n tone?: GrStatisticTone\n /**\n * Что считать хорошим: тон выводится из знака самого значения. Для выручки\n * рост — успех, для себестоимости и оттока — наоборот. Явный `tone` сильнее:\n * выведенный тон — умолчание, а не диктат.\n */\n polarity?: GrDeltaPolarity\n /** Направление динамики — задаёт цвет и иконку строки под значением. */\n trend?: GrStatisticTrend\n /** Текст динамики (например `+12,5% к прошлой неделе`). */\n trendText?: string\n /** Состояние загрузки: вместо значения — плейсхолдер. */\n loading?: boolean\n /**\n * Перебирать числа при появлении и при смене значения. Нечисловое значение\n * ставится сразу, под `prefers-reduced-motion: reduce` — тоже.\n */\n animate?: boolean\n /**\n * Длительность перебора в миллисекундах. Не токеном: шкала `--gr-duration-*`\n * заканчивается на 300 мс и описывает смену состояния, а перебор чисел —\n * другой жанр, и его число живёт там же, где твин.\n */\n animateDuration?: number\n /** Свой корневой тег (`RouterLink`, `Link` от Inertia). Сильнее `href`. */\n as?: string | Component\n /** Показатель-ссылка: переход к деталям. */\n href?: string\n /** Показатель-кнопка: интерактивна вся плитка. */\n clickable?: boolean\n}\n\nexport interface GrStatisticEmits {\n (e: 'click', event: MouseEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrStatisticProps>(),\n {\n title: undefined,\n precision: undefined,\n // Дефолты живут в `formatStatisticValue`: иначе «пользователь задал пробел»\n // было бы неотличимо от «сработал дефолт», и локаль не смогла бы победить.\n groupSeparator: undefined,\n decimalSeparator: undefined,\n locale: undefined,\n prefix: undefined,\n suffix: undefined,\n icon: undefined,\n size: undefined,\n // Дефолт тона живёт в `resolvedTone`: с ним здесь «пользователь задал\n // neutral» было бы неотличимо от «сработал дефолт», и явный\n // `tone=\"neutral\"` не смог бы отменить `polarity`.\n tone: undefined,\n polarity: undefined,\n trend: undefined,\n trendText: undefined,\n loading: false,\n animate: false,\n animateDuration: 600,\n as: undefined,\n href: undefined,\n clickable: false,\n },\n)\n\nconst emit = defineEmits<GrStatisticEmits>()\n\nconst slots = useSlots()\n\nconst { t, locale } = useGranularityTranslations()\n\nconst resolvedLocale = computed(() => props.locale ?? locale.value)\n\n/**\n * Число текущего кадра перебора. `null` — перебора нет, показываем сам проп:\n * так значение остаётся конечным и на сервере, и до первого твина.\n */\nconst frameValue = ref<number | null>(null)\n\nconst formatted = computed(() => formatStatisticValue(frameValue.value ?? props.value, {\n precision: props.precision,\n groupSeparator: props.groupSeparator,\n decimalSeparator: props.decimalSeparator,\n locale: resolvedLocale.value,\n}))\n\n/** Конечное значение — то, что читает диктор, пока на экране идёт перебор. */\nconst finalFormatted = computed(() => formatStatisticValue(props.value, {\n precision: props.precision,\n groupSeparator: props.groupSeparator,\n decimalSeparator: props.decimalSeparator,\n locale: resolvedLocale.value,\n}))\n\nconst isCounting = computed(() => frameValue.value !== null)\n\nconst hasTitle = computed(() => Boolean(props.title || slots.title))\n\nconst isInteractive = computed(() => Boolean(props.as || props.href || props.clickable))\n\nconst rootTag = computed<string | Component>(() => {\n if (props.as)\n return typeof props.as === 'string' ? props.as : markRaw(props.as)\n\n if (props.href)\n return 'a'\n\n return props.clickable ? 'button' : 'div'\n})\n\n/**\n * Компонент-ссылка (`Link` от Inertia, `RouterLink`) рендерит `<a>` сам, и без\n * `href` он ведёт в никуда. Строковый тег, кроме `a`, атрибут не понимает —\n * там он и гасится.\n */\nconst rootHref = computed(() => (\n typeof rootTag.value === 'string' && rootTag.value !== 'a' ? undefined : props.href\n))\n\nconst rootClass = computed(() => statisticRootClass({ interactive: isInteractive.value }))\n\nfunction toNumber(value: number | string): number | null {\n const numeric = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(numeric) ? numeric : null\n}\n\nlet frameHandle: number | null = null\n\nfunction stopCounting(): void {\n if (frameHandle !== null) cancelAnimationFrame(frameHandle)\n frameHandle = null\n}\n\n/**\n * Перебор чисел ведёт JS, а глобальный кламп в `base.css` гасит только CSS —\n * значит уважать «уменьшить движение» обязан сам компонент (`docs/motion.md`).\n * Читается в момент старта, а не в теле `setup`: `matchMedia` на сервере нет, и\n * первый клиентский рендер разошёлся бы с серверным.\n */\nfunction prefersReducedMotion(): boolean {\n return typeof window !== 'undefined'\n && window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true\n}\n\nfunction runCountUp(from: number, to: number): void {\n stopCounting()\n\n const duration = props.animateDuration\n if (!(duration > 0) || from === to || prefersReducedMotion()) {\n frameValue.value = null\n return\n }\n\n const started = performance.now()\n frameValue.value = from\n\n const step = (now: number): void => {\n const elapsed = now - started\n const value = countUpFrame(from, to, elapsed, duration, props.precision)\n\n if (elapsed >= duration) {\n // Конец перебора снимает и подмену значения, и подмену для диктора.\n frameValue.value = null\n frameHandle = null\n return\n }\n\n frameValue.value = value\n frameHandle = requestAnimationFrame(step)\n }\n\n frameHandle = requestAnimationFrame(step)\n}\n\n/** Перебирать можно только числа: «2 ч 15 мин» и «—» ставятся сразу. */\nfunction startIfNumeric(from: number | string, to: number | string): void {\n if (!props.animate || props.loading) {\n stopCounting()\n frameValue.value = null\n return\n }\n\n const target = toNumber(to)\n const source = toNumber(from)\n\n if (target === null || source === null) {\n stopCounting()\n frameValue.value = null\n return\n }\n\n runCountUp(source, target)\n}\n\n// Появление плитки считается переходом от нуля: ради этого эффекта проп и нужен.\nonMounted(() => startIfNumeric(0, props.value))\n\nwatch(() => props.value, (next, prev) => startIfNumeric(prev, next))\n\nonBeforeUnmount(stopCounting)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrStatistic' })\nconst resolvedPolarity = useGrComponentProp('GrStatistic', 'polarity', () => props.polarity, 'none')\n\n/**\n * Тон значения: явный проп → выведенный из знака → нейтральный.\n *\n * Отдельной ветки «полярность не задана» нет намеренно: `deltaTone(x, 'none')`\n * нейтрален при любом значении, то есть `'none'` и есть прежнее поведение.\n *\n * Знак берётся у самого пропа, а не у кадра перебора: `frameValue` стартует с\n * нуля, и цвет плитки мигал бы нейтральным на каждой анимации.\n */\nconst resolvedTone = computed<GrStatisticTone>(() => {\n if (props.tone !== undefined) return props.tone\n\n const numeric = toNumber(props.value)\n\n // «2 ч 15 мин» и «—» знака не имеют — красить нечем.\n return numeric === null ? 'neutral' : deltaTone(numeric, resolvedPolarity.value)\n})\n\nconst trendIconByTrend: Record<GrStatisticTrend, Component> = {\n up: IconTrendingUp,\n down: IconTrendingDown,\n flat: IconMinus,\n}\n\n// Иконка направления `aria-hidden`, а «+12,5 %» само по себе рост от падения не\n// отличает: направление доносит скрытая подпись.\nconst trendLabel = computed(() => {\n if (!props.trend) return undefined\n\n const labels = {\n up: () => t('gr.statistic.trendUp', 'Increase'),\n down: () => t('gr.statistic.trendDown', 'Decrease'),\n flat: () => t('gr.statistic.trendFlat', 'No change'),\n }\n\n return labels[props.trend]()\n})\n</script>\n\n<template>\n <component\n :is=\"rootTag\"\n data-gr-statistic\n :type=\"rootTag === 'button' ? 'button' : undefined\"\n :href=\"rootHref\"\n :class=\"rootClass\"\n @click=\"emit('click', $event)\"\n >\n <span\n v-if=\"icon || $slots.icon\"\n data-gr-statistic-icon\n class=\"mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-[var(--gr-radius-lg)] bg-[var(--gr-muted)] text-[var(--gr-muted-fg)]\"\n >\n <slot name=\"icon\">\n <component :is=\"iconTag(icon)\" class=\"block h-4 w-4\" :class=\"iconClass(icon)\" aria-hidden=\"true\" />\n </slot>\n </span>\n\n <div class=\"min-w-0\">\n <!--\n Подпись и значение — пара «термин — значение», а не два соседних блока.\n `<dl>` появляется только вместе с подписью: список определений без `<dt>`\n — та же бессвязность, только с претензией на семантику. Поля обнуляются\n руками: preflight пакета сбрасывает их лишь у `body`.\n -->\n <component :is=\"hasTitle ? 'dl' : 'div'\" class=\"m-0\">\n <dt\n v-if=\"hasTitle\"\n data-gr-statistic-title\n :class=\"[statisticTitleClass, statisticTitleSizeBySize[resolvedSize]]\"\n >\n <slot name=\"title\">\n {{ title }}\n </slot>\n </dt>\n\n <!-- Плейсхолдер загрузки повторяет высоту строки значения, чтобы блок не прыгал. -->\n <component\n :is=\"hasTitle ? 'dd' : 'div'\"\n v-if=\"loading\"\n data-gr-statistic-placeholder\n data-testid=\"gr-statistic-placeholder\"\n class=\"m-0 mt-1\"\n role=\"status\"\n aria-busy=\"true\"\n >\n <GrSkeleton\n width=\"6rem\"\n :height=\"statisticPlaceholderHeightBySize[resolvedSize]\"\n rounded=\"var(--gr-radius-md)\"\n />\n <span class=\"sr-only\">{{ t('gr.loading.defaultText', 'Loading...') }}</span>\n </component>\n\n <component\n :is=\"hasTitle ? 'dd' : 'div'\"\n v-else\n data-gr-statistic-value\n data-testid=\"gr-statistic-value\"\n class=\"m-0 mt-0.5 flex items-baseline gap-1\"\n >\n <span\n v-if=\"prefix || $slots.prefix\"\n data-gr-statistic-prefix\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"statisticAffixSizeBySize[resolvedSize]\"\n >\n <slot name=\"prefix\">{{ prefix }}</slot>\n </span>\n\n <span\n data-gr-statistic-number\n :class=\"statisticValueClass({ size: resolvedSize, tone: resolvedTone })\"\n :aria-hidden=\"isCounting ? 'true' : undefined\"\n >\n <slot>{{ formatted }}</slot>\n </span>\n\n <!--\n Пока идёт перебор, на экране промежуточное число. Диктору отдаётся\n конечное: «1 284» глазами и «743» в ушах — не шум, а неверные данные.\n -->\n <span v-if=\"isCounting\" data-gr-statistic-final class=\"sr-only\">{{ finalFormatted }}</span>\n\n <span\n v-if=\"suffix || $slots.suffix\"\n data-gr-statistic-suffix\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"statisticAffixSizeBySize[resolvedSize]\"\n >\n <slot name=\"suffix\">{{ suffix }}</slot>\n </span>\n </component>\n </component>\n\n <div\n v-if=\"trend || trendText || $slots.trend\"\n data-gr-statistic-trend\n data-testid=\"gr-statistic-trend\"\n class=\"mt-1 inline-flex items-center gap-1\"\n :class=\"[statisticTrendSizeBySize[resolvedSize], statisticTrendClassByTrend[trend ?? 'flat']]\"\n >\n <span v-if=\"trendLabel\" data-gr-statistic-trend-label class=\"sr-only\">{{ trendLabel }}</span>\n\n <slot name=\"trend\">\n <component\n :is=\"trendIconByTrend[trend]\"\n v-if=\"trend\"\n class=\"block h-3.5 w-3.5\"\n aria-hidden=\"true\"\n />\n <span>{{ trendText }}</span>\n </slot>\n </div>\n </div>\n </component>\n</template>\n","<script setup lang=\"ts\">\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { computed, markRaw, onBeforeUnmount, onMounted, ref, useSlots, watch, type Component } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { deltaTone, type GrDeltaPolarity } from '../GrDelta/deltaTone'\nimport { iconClass, iconTag } from '../shared/icon'\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\n\nimport IconMinus from '~icons/lucide/minus'\nimport IconTrendingDown from '~icons/lucide/trending-down'\nimport IconTrendingUp from '~icons/lucide/trending-up'\n\nimport { countUpFrame } from './countUp'\nimport { formatStatisticValue } from './formatStatisticValue'\n\nimport {\n statisticAffixSizeBySize,\n statisticPlaceholderHeightBySize,\n statisticRootClass,\n statisticTitleClass,\n statisticTitleSizeBySize,\n statisticTrendClassByTrend,\n statisticTrendSizeBySize,\n statisticValueClass,\n type GrStatisticSize,\n type GrStatisticTone,\n type GrStatisticTrend,\n} from './grStatisticStyles'\n\nexport type {\n GrStatisticSize,\n GrStatisticTone,\n GrStatisticTrend,\n} from './grStatisticStyles'\nexport type { GrStatisticFormatOptions } from './formatStatisticValue'\n\n/**\n * Публичный GR-примитив «Statistic» — крупный числовой показатель с подписью,\n * префиксом/суффиксом и строкой динамики.\n *\n * Компонент презентационный: не грузит данные и не считает дельту, только\n * форматирует и подаёт. Форматирование — чистая функция `formatStatisticValue`.\n */\nexport interface GrStatisticProps {\n /** Значение показателя. Нечисловая строка выводится как есть. */\n value: number | string\n /** Подпись над значением. */\n title?: string\n /** Число знаков после запятой. */\n precision?: number\n /** Разделитель разрядов. Сильнее локали; без него и без локали — узкий пробел. */\n groupSeparator?: string\n /** Десятичный разделитель. Сильнее локали; без него и без локали — точка. */\n decimalSeparator?: string\n /**\n * BCP-47 локаль форматирования. Не задана — берётся из i18n-адаптера; нет и\n * адаптера — работают ручные разделители.\n */\n locale?: string\n /** Приписка перед значением (валюта, знак). */\n prefix?: string\n /** Приписка после значения (единица измерения, `%`). */\n suffix?: string\n /**\n * Иконка слева от блока: Vue-компонент либо класс иконки вашей UnoCSS-сборки\n * (`'i-lucide-users'` — тогда нужен ваш `presetIcons`, см. `docs/installation.md`).\n */\n icon?: string | Component\n size?: GrStatisticSize\n /** Тон значения; точечно перекрывается `--gr-statistic-value-color`. */\n tone?: GrStatisticTone\n /**\n * Что считать хорошим: тон выводится из знака самого значения. Для выручки\n * рост — успех, для себестоимости и оттока — наоборот. Явный `tone` сильнее:\n * выведенный тон — умолчание, а не диктат.\n */\n polarity?: GrDeltaPolarity\n /** Направление динамики — задаёт цвет и иконку строки под значением. */\n trend?: GrStatisticTrend\n /** Текст динамики (например `+12,5% к прошлой неделе`). */\n trendText?: string\n /** Состояние загрузки: вместо значения — плейсхолдер. */\n loading?: boolean\n /**\n * Перебирать числа при появлении и при смене значения. Нечисловое значение\n * ставится сразу, под `prefers-reduced-motion: reduce` — тоже.\n */\n animate?: boolean\n /**\n * Длительность перебора в миллисекундах. Не токеном: шкала `--gr-duration-*`\n * заканчивается на 300 мс и описывает смену состояния, а перебор чисел —\n * другой жанр, и его число живёт там же, где твин.\n */\n animateDuration?: number\n /** Свой корневой тег (`RouterLink`, `Link` от Inertia). Сильнее `href`. */\n as?: string | Component\n /** Показатель-ссылка: переход к деталям. */\n href?: string\n /** Показатель-кнопка: интерактивна вся плитка. */\n clickable?: boolean\n}\n\nexport interface GrStatisticEmits {\n (e: 'click', event: MouseEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrStatisticProps>(),\n {\n title: undefined,\n precision: undefined,\n // Дефолты живут в `formatStatisticValue`: иначе «пользователь задал пробел»\n // было бы неотличимо от «сработал дефолт», и локаль не смогла бы победить.\n groupSeparator: undefined,\n decimalSeparator: undefined,\n locale: undefined,\n prefix: undefined,\n suffix: undefined,\n icon: undefined,\n size: undefined,\n // Дефолт тона живёт в `resolvedTone`: с ним здесь «пользователь задал\n // neutral» было бы неотличимо от «сработал дефолт», и явный\n // `tone=\"neutral\"` не смог бы отменить `polarity`.\n tone: undefined,\n polarity: undefined,\n trend: undefined,\n trendText: undefined,\n loading: false,\n animate: false,\n animateDuration: 600,\n as: undefined,\n href: undefined,\n clickable: false,\n },\n)\n\nconst emit = defineEmits<GrStatisticEmits>()\n\nconst slots = useSlots()\n\nconst { t, locale } = useGranularityTranslations()\n\nconst resolvedLocale = computed(() => props.locale ?? locale.value)\n\n/**\n * Число текущего кадра перебора. `null` — перебора нет, показываем сам проп:\n * так значение остаётся конечным и на сервере, и до первого твина.\n */\nconst frameValue = ref<number | null>(null)\n\nconst formatted = computed(() => formatStatisticValue(frameValue.value ?? props.value, {\n precision: props.precision,\n groupSeparator: props.groupSeparator,\n decimalSeparator: props.decimalSeparator,\n locale: resolvedLocale.value,\n}))\n\n/** Конечное значение — то, что читает диктор, пока на экране идёт перебор. */\nconst finalFormatted = computed(() => formatStatisticValue(props.value, {\n precision: props.precision,\n groupSeparator: props.groupSeparator,\n decimalSeparator: props.decimalSeparator,\n locale: resolvedLocale.value,\n}))\n\nconst isCounting = computed(() => frameValue.value !== null)\n\nconst hasTitle = computed(() => Boolean(props.title || slots.title))\n\nconst isInteractive = computed(() => Boolean(props.as || props.href || props.clickable))\n\nconst rootTag = computed<string | Component>(() => {\n if (props.as)\n return typeof props.as === 'string' ? props.as : markRaw(props.as)\n\n if (props.href)\n return 'a'\n\n return props.clickable ? 'button' : 'div'\n})\n\n/**\n * Компонент-ссылка (`Link` от Inertia, `RouterLink`) рендерит `<a>` сам, и без\n * `href` он ведёт в никуда. Строковый тег, кроме `a`, атрибут не понимает —\n * там он и гасится.\n */\nconst rootHref = computed(() => (\n typeof rootTag.value === 'string' && rootTag.value !== 'a' ? undefined : props.href\n))\n\nconst rootClass = computed(() => statisticRootClass({ interactive: isInteractive.value }))\n\nfunction toNumber(value: number | string): number | null {\n const numeric = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(numeric) ? numeric : null\n}\n\nlet frameHandle: number | null = null\n\nfunction stopCounting(): void {\n if (frameHandle !== null) cancelAnimationFrame(frameHandle)\n frameHandle = null\n}\n\n/**\n * Перебор чисел ведёт JS, а глобальный кламп в `base.css` гасит только CSS —\n * значит уважать «уменьшить движение» обязан сам компонент (`docs/motion.md`).\n * Читается в момент старта, а не в теле `setup`: `matchMedia` на сервере нет, и\n * первый клиентский рендер разошёлся бы с серверным.\n */\nfunction prefersReducedMotion(): boolean {\n return typeof window !== 'undefined'\n && window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true\n}\n\nfunction runCountUp(from: number, to: number): void {\n stopCounting()\n\n const duration = props.animateDuration\n if (!(duration > 0) || from === to || prefersReducedMotion()) {\n frameValue.value = null\n return\n }\n\n const started = performance.now()\n frameValue.value = from\n\n const step = (now: number): void => {\n const elapsed = now - started\n const value = countUpFrame(from, to, elapsed, duration, props.precision)\n\n if (elapsed >= duration) {\n // Конец перебора снимает и подмену значения, и подмену для диктора.\n frameValue.value = null\n frameHandle = null\n return\n }\n\n frameValue.value = value\n frameHandle = requestAnimationFrame(step)\n }\n\n frameHandle = requestAnimationFrame(step)\n}\n\n/** Перебирать можно только числа: «2 ч 15 мин» и «—» ставятся сразу. */\nfunction startIfNumeric(from: number | string, to: number | string): void {\n if (!props.animate || props.loading) {\n stopCounting()\n frameValue.value = null\n return\n }\n\n const target = toNumber(to)\n const source = toNumber(from)\n\n if (target === null || source === null) {\n stopCounting()\n frameValue.value = null\n return\n }\n\n runCountUp(source, target)\n}\n\n// Появление плитки считается переходом от нуля: ради этого эффекта проп и нужен.\nonMounted(() => startIfNumeric(0, props.value))\n\nwatch(() => props.value, (next, prev) => startIfNumeric(prev, next))\n\nonBeforeUnmount(stopCounting)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrStatistic' })\nconst resolvedPolarity = useGrComponentProp('GrStatistic', 'polarity', () => props.polarity, 'none')\n\n/**\n * Тон значения: явный проп → выведенный из знака → нейтральный.\n *\n * Отдельной ветки «полярность не задана» нет намеренно: `deltaTone(x, 'none')`\n * нейтрален при любом значении, то есть `'none'` и есть прежнее поведение.\n *\n * Знак берётся у самого пропа, а не у кадра перебора: `frameValue` стартует с\n * нуля, и цвет плитки мигал бы нейтральным на каждой анимации.\n */\nconst resolvedTone = computed<GrStatisticTone>(() => {\n if (props.tone !== undefined) return props.tone\n\n const numeric = toNumber(props.value)\n\n // «2 ч 15 мин» и «—» знака не имеют — красить нечем.\n return numeric === null ? 'neutral' : deltaTone(numeric, resolvedPolarity.value)\n})\n\nconst trendIconByTrend: Record<GrStatisticTrend, Component> = {\n up: IconTrendingUp,\n down: IconTrendingDown,\n flat: IconMinus,\n}\n\n// Иконка направления `aria-hidden`, а «+12,5 %» само по себе рост от падения не\n// отличает: направление доносит скрытая подпись.\nconst trendLabel = computed(() => {\n if (!props.trend) return undefined\n\n const labels = {\n up: () => t('gr.statistic.trendUp', 'Increase'),\n down: () => t('gr.statistic.trendDown', 'Decrease'),\n flat: () => t('gr.statistic.trendFlat', 'No change'),\n }\n\n return labels[props.trend]()\n})\n</script>\n\n<template>\n <component\n :is=\"rootTag\"\n data-gr-statistic\n :type=\"rootTag === 'button' ? 'button' : undefined\"\n :href=\"rootHref\"\n :class=\"rootClass\"\n @click=\"emit('click', $event)\"\n >\n <span\n v-if=\"icon || $slots.icon\"\n data-gr-statistic-icon\n class=\"mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-[var(--gr-radius-lg)] bg-[var(--gr-muted)] text-[var(--gr-muted-fg)]\"\n >\n <slot name=\"icon\">\n <component :is=\"iconTag(icon)\" class=\"block h-4 w-4\" :class=\"iconClass(icon)\" aria-hidden=\"true\" />\n </slot>\n </span>\n\n <div class=\"min-w-0\">\n <!--\n Подпись и значение — пара «термин — значение», а не два соседних блока.\n `<dl>` появляется только вместе с подписью: список определений без `<dt>`\n — та же бессвязность, только с претензией на семантику. Поля обнуляются\n руками: preflight пакета сбрасывает их лишь у `body`.\n -->\n <component :is=\"hasTitle ? 'dl' : 'div'\" class=\"m-0\">\n <dt\n v-if=\"hasTitle\"\n data-gr-statistic-title\n :class=\"[statisticTitleClass, statisticTitleSizeBySize[resolvedSize]]\"\n >\n <slot name=\"title\">\n {{ title }}\n </slot>\n </dt>\n\n <!-- Плейсхолдер загрузки повторяет высоту строки значения, чтобы блок не прыгал. -->\n <component\n :is=\"hasTitle ? 'dd' : 'div'\"\n v-if=\"loading\"\n data-gr-statistic-placeholder\n data-testid=\"gr-statistic-placeholder\"\n class=\"m-0 mt-1\"\n role=\"status\"\n aria-busy=\"true\"\n >\n <GrSkeleton\n width=\"6rem\"\n :height=\"statisticPlaceholderHeightBySize[resolvedSize]\"\n rounded=\"var(--gr-radius-md)\"\n />\n <span class=\"sr-only\">{{ t('gr.loading.defaultText', 'Loading...') }}</span>\n </component>\n\n <component\n :is=\"hasTitle ? 'dd' : 'div'\"\n v-else\n data-gr-statistic-value\n data-testid=\"gr-statistic-value\"\n class=\"m-0 mt-0.5 flex items-baseline gap-1\"\n >\n <span\n v-if=\"prefix || $slots.prefix\"\n data-gr-statistic-prefix\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"statisticAffixSizeBySize[resolvedSize]\"\n >\n <slot name=\"prefix\">{{ prefix }}</slot>\n </span>\n\n <span\n data-gr-statistic-number\n :class=\"statisticValueClass({ size: resolvedSize, tone: resolvedTone })\"\n :aria-hidden=\"isCounting ? 'true' : undefined\"\n >\n <slot>{{ formatted }}</slot>\n </span>\n\n <!--\n Пока идёт перебор, на экране промежуточное число. Диктору отдаётся\n конечное: «1 284» глазами и «743» в ушах — не шум, а неверные данные.\n -->\n <span v-if=\"isCounting\" data-gr-statistic-final class=\"sr-only\">{{ finalFormatted }}</span>\n\n <span\n v-if=\"suffix || $slots.suffix\"\n data-gr-statistic-suffix\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"statisticAffixSizeBySize[resolvedSize]\"\n >\n <slot name=\"suffix\">{{ suffix }}</slot>\n </span>\n </component>\n </component>\n\n <div\n v-if=\"trend || trendText || $slots.trend\"\n data-gr-statistic-trend\n data-testid=\"gr-statistic-trend\"\n class=\"mt-1 inline-flex items-center gap-1\"\n :class=\"[statisticTrendSizeBySize[resolvedSize], statisticTrendClassByTrend[trend ?? 'flat']]\"\n >\n <span v-if=\"trendLabel\" data-gr-statistic-trend-label class=\"sr-only\">{{ trendLabel }}</span>\n\n <slot name=\"trend\">\n <component\n :is=\"trendIconByTrend[trend]\"\n v-if=\"trend\"\n class=\"block h-3.5 w-3.5\"\n aria-hidden=\"true\"\n />\n <span>{{ trendText }}</span>\n </slot>\n </div>\n </div>\n </component>\n</template>\n"],"mappings":"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"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|