@feugene/granularity 0.28.0 → 0.28.2
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-DONFPTFg.js → config-C0Eu8jpG.js} +4 -1
- package/dist/chunks/config-C0Eu8jpG.js.map +1 -0
- package/dist/chunks/{config-CWthWcWY.js → config-_jbgsFpw.js} +2 -1
- package/dist/chunks/config-_jbgsFpw.js.map +1 -0
- package/dist/chunks/{grDropdownMenuStyles-DG3aYHkT.js → grDropdownMenuStyles-Cf8OdR8S.js} +20 -3
- package/dist/chunks/grDropdownMenuStyles-Cf8OdR8S.js.map +1 -0
- package/dist/chunks/{granular-provider-BhBBIAl7.js → granular-provider-Dds_519x.js} +2 -2
- package/dist/chunks/{granular-provider-BhBBIAl7.js.map → granular-provider-Dds_519x.js.map} +1 -1
- package/dist/components/GrBreadcrumbs/chunks/{GrBreadcrumbs-BYbfo-49.js → GrBreadcrumbs-BKOvl8W1.js} +19 -3
- package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-BKOvl8W1.js.map +1 -0
- package/dist/components/GrBreadcrumbs/index.js +2 -2
- package/dist/components/GrContextMenu/chunks/{GrContextMenu-DDAUvrDK.js → GrContextMenu-BNQDbAsY.js} +1 -1
- package/dist/components/GrContextMenu/chunks/{GrContextMenu-DDAUvrDK.js.map → GrContextMenu-BNQDbAsY.js.map} +1 -1
- package/dist/components/GrContextMenu/index.js +1 -1
- package/dist/components/GrDelta/chunks/{GrDelta-04j9WmEY.js → GrDelta-BZwkgQW-.js} +0 -0
- package/dist/components/GrDelta/chunks/GrDelta-BZwkgQW-.js.map +1 -0
- package/dist/components/GrDelta/index.js +1 -1
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-5zoOL8Oj.js → GrDropdownMenu-Cg7qHV9J.js} +2 -2
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-5zoOL8Oj.js.map → GrDropdownMenu-Cg7qHV9J.js.map} +1 -1
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenuList-CT_1TQJo.js → GrDropdownMenuList-RW5IBvmu.js} +1 -1
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenuList-CT_1TQJo.js.map → GrDropdownMenuList-RW5IBvmu.js.map} +1 -1
- package/dist/components/GrDropdownMenu/index.js +3 -3
- package/dist/directives/clickOutside.js +83 -21
- package/dist/directives/clickOutside.js.map +1 -1
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/index.js +7 -7
- package/dist/types/components/GrBreadcrumbs/grBreadcrumbsStyles.d.ts +12 -0
- package/dist/types/components/GrDropdownMenu/grDropdownMenuStyles.d.ts +18 -2
- package/dist/types/internal/granularityFormat.d.ts +2 -2
- package/dist/web-types.json +1 -1
- package/package.json +85 -4
- package/dist/chunks/config-CWthWcWY.js.map +0 -1
- package/dist/chunks/config-DONFPTFg.js.map +0 -1
- package/dist/chunks/grDropdownMenuStyles-DG3aYHkT.js.map +0 -1
- package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-BYbfo-49.js.map +0 -1
- package/dist/components/GrDelta/chunks/GrDelta-04j9WmEY.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrBreadcrumbs-BYbfo-49.js","names":[],"sources":["../../../../src/components/GrBreadcrumbs/GrBreadcrumbs.vue","../../../../src/components/GrBreadcrumbs/GrBreadcrumbs.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport GrLink from '../GrLink/GrLink.vue'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { iconClass, iconTag } from '../shared/icon'\n\nimport {\n breadcrumbsCurrentClass,\n breadcrumbsEllipsisClass,\n breadcrumbsItemIconClass,\n breadcrumbsItemWrapClass,\n breadcrumbsLabelClass,\n breadcrumbsListClass,\n breadcrumbsListNowrapClass,\n breadcrumbsListWrapClass,\n breadcrumbsRootClass,\n breadcrumbsSeparatorClass,\n breadcrumbsSizeClassBySize,\n type GrBreadcrumbItem,\n type GrBreadcrumbsLinkComponent,\n type GrBreadcrumbsSize,\n resolveBreadcrumbsFit,\n resolveBreadcrumbsLayout,\n} from './grBreadcrumbsStyles'\n\nexport type { GrBreadcrumbItem, GrBreadcrumbsSize } from './grBreadcrumbsStyles'\n\n/**\n * Путь до текущей страницы (WAI-ARIA breadcrumb): `<nav>` с именем, внутри\n * упорядоченный список ссылок, последний пункт — текущая страница.\n *\n * Ссылки рисует `GrLink`, поэтому роутер подключается тем же способом, что и\n * везде в пакете: `as` + `to`.\n */\nexport interface GrBreadcrumbsProps {\n /** Путь от корня к текущей странице. Последний пункт — текущая страница. */\n items: GrBreadcrumbItem[]\n /** Компонент ссылки для всех пунктов: `RouterLink`, `NuxtLink`, `Link` Inertia. */\n as?: GrBreadcrumbsLinkComponent\n /** Разделитель между пунктами. Декоративен: диктору не читается. */\n separator?: string\n size?: GrBreadcrumbsSize\n /**\n * С какого числа пунктов схлопывать середину в «…». `0`/не задано — показывать\n * путь целиком.\n */\n maxItems?: number\n /** Сколько пунктов оставить в начале при схлопывании. */\n itemsBeforeCollapse?: number\n /** Сколько пунктов оставить в конце при схлопывании. */\n itemsAfterCollapse?: number\n /** Имя лендмарка. Не задано — берётся из локали. */\n ariaLabel?: string\n /** Последний пункт остаётся ссылкой (текущая страница кликабельна). */\n linkCurrent?: boolean\n /**\n * Какой пункт — текущая страница. Не задан — последний.\n *\n * `-1` означает, что текущей страницы в пути нет: её имя стоит в `h1`, а\n * путь показывает только родителей. Тогда `aria-current` не получает никто,\n * и последний пункт остаётся обычной ссылкой.\n */\n currentIndex?: number\n /** i18n-метка кнопки раскрытия схлопнутого пути. */\n expandLabel?: string\n /**\n * Схлопывать по доступной ширине, а не по числу пунктов. Путь становится\n * однострочным, середина уходит под «…» ровно настолько, насколько не влезает.\n *\n * Без пропа поведение прежнее: список переносится на вторую строку, а порог\n * задаёт `maxItems`. Вместе пропы совместимы — `maxItems` остаётся жёстким\n * потолком, ширина ужимает дальше.\n */\n autoCollapse?: boolean\n}\n\nconst props = withDefaults(defineProps<GrBreadcrumbsProps>(), {\n as: undefined,\n separator: undefined,\n size: undefined,\n maxItems: undefined,\n itemsBeforeCollapse: 1,\n itemsAfterCollapse: 1,\n ariaLabel: undefined,\n linkCurrent: false,\n currentIndex: undefined,\n expandLabel: undefined,\n autoCollapse: false,\n})\n\ndefineSlots<{\n /** Содержимое пункта целиком (иконка и подпись). */\n item?: (props: { item: GrBreadcrumbItem, index: number, isCurrent: boolean }) => unknown\n /** Разделитель между пунктами. */\n separator?: () => unknown\n /** Кнопка раскрытия схлопнутой середины. */\n ellipsis?: (props: { hiddenCount: number, expand: () => void }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrBreadcrumbs' })\nconst resolvedSeparator = useGrComponentProp('GrBreadcrumbs', 'separator', () => props.separator, '/')\n\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.breadcrumbs.label', 'Breadcrumb'))\nconst resolvedExpandLabel = computed(() => props.expandLabel ?? t('gr.breadcrumbs.expand', 'Show hidden path'))\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst listEl = ref<HTMLElement | null>(null)\nconst expanded = ref(false)\n\n/**\n * Схлопывание по ширине: сколько пунктов хвоста влезает.\n *\n * `undefined` — ещё не мерили, показываем путь целиком. Первый кадр в режиме\n * `autoCollapse` намеренно полный: ширины снимаются с него, а посчитать их до\n * рендера неоткуда.\n */\nconst fitAfter = ref<number | undefined>(undefined)\n\n/** Ширины не зависят от контейнера, поэтому живут до смены самого пути. */\nlet measured: { items: number[], separator: number } | null = null\n/**\n * Ширина кнопки «…» узнаётся только когда она отрисована, то есть уже после\n * первого схлопывания. До этого считаем её нулевой: поправка может лишь\n * ужать хвост ещё на пункт, а не вернуть спрятанное, поэтому качелей не будет.\n */\nlet ellipsisWidth = 0\n/** Ширина, на которой считали в прошлый раз: по её смене раскрытый путь сворачивается. */\nlet lastAvailable = 0\n\n// Новый путь — новая страница: раскрытая середина прошлого пути к ней отношения\n// не имеет, а измеренные ширины относятся к прошлым подписям.\nwatch(() => props.items, () => {\n expanded.value = false\n measured = null\n ellipsisWidth = 0\n fitAfter.value = undefined\n void nextTick(measure)\n})\n\nconst lastIndex = computed(() => props.items.length - 1)\n\n/**\n * Схлопывание по ширине выражается через тот же `maxItems`: раскладку считает\n * одна функция, а режимы отличаются только тем, откуда взялось число.\n */\nconst autoMaxItems = computed(() => {\n if (!props.autoCollapse || fitAfter.value === undefined) return undefined\n if (fitAfter.value >= props.items.length) return undefined\n\n return props.items.length - 1\n})\n\nconst effectiveMaxItems = computed(() => {\n const limits = [props.maxItems, autoMaxItems.value].filter((value): value is number => typeof value === 'number' && value > 0)\n\n return limits.length > 0 ? Math.min(...limits) : undefined\n})\n\nconst effectiveAfter = computed(() => (\n props.autoCollapse && fitAfter.value !== undefined ? fitAfter.value : props.itemsAfterCollapse\n))\n\nconst entries = computed(() => resolveBreadcrumbsLayout({\n items: props.items,\n maxItems: effectiveMaxItems.value,\n itemsBeforeCollapse: props.itemsBeforeCollapse,\n itemsAfterCollapse: effectiveAfter.value,\n expanded: expanded.value,\n}))\n\nconst resolvedCurrentIndex = computed(() => props.currentIndex ?? lastIndex.value)\n\nfunction isCurrent(index: number): boolean {\n return index === resolvedCurrentIndex.value\n}\n\n/**\n * Ссылкой пункт становится, только если ведёт куда-то и не выключен.\n */\nfunction isLink(item: GrBreadcrumbItem, index: number): boolean {\n if (item.disabled) return false\n if (isCurrent(index) && !props.linkCurrent) return false\n return item.href !== undefined || item.to !== undefined\n}\n\nfunction widthOf(el: Element | null | undefined): number {\n // `scrollWidth`, а не прямоугольник: у подписи стоит `truncate`, и в узком\n // контейнере прямоугольник вернул бы уже ужатую ширину, а не собственную.\n return el ? (el as HTMLElement).scrollWidth : 0\n}\n\n/**\n * Снять ширины с полного пути и пересчитать, сколько пунктов хвоста влезает.\n *\n * Замер делается один раз на путь: подписи от ширины контейнера не зависят,\n * поэтому `resize` пересчитывает только арифметику.\n */\nfunction measure(): void {\n if (!props.autoCollapse) return\n\n const list = listEl.value\n if (!list) return\n\n const ellipsis = list.querySelector('[data-gr-breadcrumbs-ellipsis-item]')\n if (ellipsis) ellipsisWidth = widthOf(ellipsis)\n\n if (!measured) {\n // Ширины снимаем только с полного пути: спрятанных пунктов в DOM нет.\n if (fitAfter.value !== undefined) return\n\n const wraps = [...list.querySelectorAll('[data-gr-breadcrumbs-item-wrap]')]\n if (wraps.length !== props.items.length) return\n\n measured = {\n items: wraps.map(widthOf),\n separator: widthOf(list.querySelector('[data-gr-breadcrumbs-separator]')),\n }\n }\n\n const available = list.clientWidth\n // jsdom и скрытый контейнер отдают 0 — решать по такой ширине нечего.\n if (available <= 0) return\n\n lastAvailable = available\n\n fitAfter.value = resolveBreadcrumbsFit({\n itemWidths: measured.items,\n separatorWidth: measured.separator,\n ellipsisWidth,\n available,\n itemsBeforeCollapse: props.itemsBeforeCollapse,\n gapWidth: listGap(list),\n })\n}\n\n/** Зазор списка в пикселях: он задан классом, а не пропом, поэтому спрашиваем стиль. */\nfunction listGap(list: HTMLElement): number {\n if (typeof getComputedStyle !== 'function') return 0\n\n return Number.parseFloat(getComputedStyle(list).columnGap) || 0\n}\n\n/**\n * Ширина кнопки «…» известна только после того, как она отрисована, то есть\n * после первого схлопывания. Поэтому решение пересчитывается ещё раз: поправка\n * либо ничего не меняет, либо ужимает хвост на пункт. Цикла нет — второй проход\n * оставляет `fitAfter` прежним, и наблюдатель больше не срабатывает.\n */\nwatch(fitAfter, () => void nextTick(measure))\n\nlet observer: ResizeObserver | null = null\n\nwatch(() => props.autoCollapse, (enabled) => {\n if (enabled) {\n void nextTick(measure)\n return\n }\n\n measured = null\n ellipsisWidth = 0\n fitAfter.value = undefined\n})\n\nonMounted(() => {\n void nextTick(measure)\n\n // На сервере и в jsdom `ResizeObserver` отсутствует — измерять там нечего.\n if (typeof ResizeObserver === 'undefined') return\n\n observer = new ResizeObserver(() => {\n // Смена ширины — новая раскладка, и раскрытая руками середина относилась к\n // прежней. Свернуть её иначе нечем: в раскрытом пути кнопки «…» нет, то есть\n // поворот телефона оставлял бы путь развёрнутым навсегда. Высота меняется от\n // самого раскрытия (путь переносится на вторую строку) — на неё не реагируем.\n if (expanded.value && listEl.value && listEl.value.clientWidth !== lastAvailable)\n expanded.value = false\n\n measure()\n })\n if (listEl.value) observer.observe(listEl.value)\n})\n\nonBeforeUnmount(() => observer?.disconnect())\n\nasync function expand(): Promise<void> {\n expanded.value = true\n\n // Кнопка исчезает вместе со схлопыванием, и фокус ушёл бы на `<body>`.\n // Переводим его на первый раскрытый пункт — там, где кнопка и стояла. Пункты\n // без ссылки держат `tabindex=\"-1\"` именно ради этого случая.\n await nextTick()\n const before = Math.max(0, Math.min(props.itemsBeforeCollapse, props.items.length))\n const revealed = rootEl.value?.querySelectorAll<HTMLElement>('[data-gr-breadcrumbs-item]')\n revealed?.[before]?.focus()\n}\n</script>\n\n<template>\n <nav\n ref=\"rootEl\"\n data-gr-breadcrumbs\n :class=\"[breadcrumbsRootClass, breadcrumbsSizeClassBySize[resolvedSize]]\"\n :aria-label=\"resolvedAriaLabel\"\n >\n <ol\n ref=\"listEl\"\n data-gr-breadcrumbs-list\n :class=\"[breadcrumbsListClass, autoCollapse && !expanded ? breadcrumbsListNowrapClass : breadcrumbsListWrapClass]\"\n >\n <template v-for=\"(entry, position) in entries\" :key=\"entry.kind === 'item' ? `item-${entry.index}` : 'ellipsis'\">\n <!--\n Разделитель — отдельный элемент списка и помечен `aria-hidden`:\n структуру пути диктору сообщает сам список, а «/» он бы читал вслух.\n -->\n <li\n v-if=\"position > 0\"\n data-gr-breadcrumbs-separator\n :class=\"breadcrumbsSeparatorClass\"\n aria-hidden=\"true\"\n >\n <slot name=\"separator\">\n {{ resolvedSeparator }}\n </slot>\n </li>\n\n <li v-if=\"entry.kind === 'ellipsis'\" data-gr-breadcrumbs-ellipsis-item class=\"min-w-0\">\n <slot name=\"ellipsis\" :hidden-count=\"entry.hiddenCount\" :expand=\"expand\">\n <button\n type=\"button\"\n data-gr-breadcrumbs-ellipsis\n data-testid=\"gr-breadcrumbs-ellipsis\"\n :class=\"breadcrumbsEllipsisClass\"\n :aria-label=\"resolvedExpandLabel\"\n @click=\"expand\"\n >\n …\n </button>\n </slot>\n </li>\n\n <li v-else data-gr-breadcrumbs-item-wrap :class=\"breadcrumbsItemWrapClass\">\n <GrLink\n v-if=\"isLink(entry.item, entry.index)\"\n :key=\"`link-${entry.index}`\"\n data-gr-breadcrumbs-item\n :as=\"as\"\n :href=\"entry.item.href\"\n :to=\"entry.item.to\"\n :size=\"resolvedSize\"\n :variant=\"isCurrent(entry.index) ? 'default' : 'muted'\"\n :aria-label=\"entry.item.ariaLabel\"\n :aria-current=\"isCurrent(entry.index) ? 'page' : undefined\"\n >\n <slot name=\"item\" :item=\"entry.item\" :index=\"entry.index\" :is-current=\"isCurrent(entry.index)\">\n <component\n :is=\"iconTag(entry.item.icon)\"\n v-if=\"entry.item.icon\"\n :class=\"[breadcrumbsItemIconClass, iconClass(entry.item.icon)]\"\n aria-hidden=\"true\"\n />\n <span :class=\"breadcrumbsLabelClass\">{{ entry.item.label }}</span>\n </slot>\n </GrLink>\n\n <!--\n Текущая страница и выключенный пункт — не ссылки. `aria-current`\n остаётся на текущем: именно он отвечает на вопрос «где я».\n -->\n <span\n v-else\n data-gr-breadcrumbs-item\n :class=\"breadcrumbsCurrentClass\"\n :aria-label=\"entry.item.ariaLabel\"\n :aria-current=\"isCurrent(entry.index) ? 'page' : undefined\"\n :aria-disabled=\"entry.item.disabled ? 'true' : undefined\"\n tabindex=\"-1\"\n >\n <slot name=\"item\" :item=\"entry.item\" :index=\"entry.index\" :is-current=\"isCurrent(entry.index)\">\n <component\n :is=\"iconTag(entry.item.icon)\"\n v-if=\"entry.item.icon\"\n :class=\"[breadcrumbsItemIconClass, iconClass(entry.item.icon)]\"\n aria-hidden=\"true\"\n />\n <span :class=\"breadcrumbsLabelClass\">{{ entry.item.label }}</span>\n </slot>\n </span>\n </li>\n </template>\n </ol>\n </nav>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport GrLink from '../GrLink/GrLink.vue'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { iconClass, iconTag } from '../shared/icon'\n\nimport {\n breadcrumbsCurrentClass,\n breadcrumbsEllipsisClass,\n breadcrumbsItemIconClass,\n breadcrumbsItemWrapClass,\n breadcrumbsLabelClass,\n breadcrumbsListClass,\n breadcrumbsListNowrapClass,\n breadcrumbsListWrapClass,\n breadcrumbsRootClass,\n breadcrumbsSeparatorClass,\n breadcrumbsSizeClassBySize,\n type GrBreadcrumbItem,\n type GrBreadcrumbsLinkComponent,\n type GrBreadcrumbsSize,\n resolveBreadcrumbsFit,\n resolveBreadcrumbsLayout,\n} from './grBreadcrumbsStyles'\n\nexport type { GrBreadcrumbItem, GrBreadcrumbsSize } from './grBreadcrumbsStyles'\n\n/**\n * Путь до текущей страницы (WAI-ARIA breadcrumb): `<nav>` с именем, внутри\n * упорядоченный список ссылок, последний пункт — текущая страница.\n *\n * Ссылки рисует `GrLink`, поэтому роутер подключается тем же способом, что и\n * везде в пакете: `as` + `to`.\n */\nexport interface GrBreadcrumbsProps {\n /** Путь от корня к текущей странице. Последний пункт — текущая страница. */\n items: GrBreadcrumbItem[]\n /** Компонент ссылки для всех пунктов: `RouterLink`, `NuxtLink`, `Link` Inertia. */\n as?: GrBreadcrumbsLinkComponent\n /** Разделитель между пунктами. Декоративен: диктору не читается. */\n separator?: string\n size?: GrBreadcrumbsSize\n /**\n * С какого числа пунктов схлопывать середину в «…». `0`/не задано — показывать\n * путь целиком.\n */\n maxItems?: number\n /** Сколько пунктов оставить в начале при схлопывании. */\n itemsBeforeCollapse?: number\n /** Сколько пунктов оставить в конце при схлопывании. */\n itemsAfterCollapse?: number\n /** Имя лендмарка. Не задано — берётся из локали. */\n ariaLabel?: string\n /** Последний пункт остаётся ссылкой (текущая страница кликабельна). */\n linkCurrent?: boolean\n /**\n * Какой пункт — текущая страница. Не задан — последний.\n *\n * `-1` означает, что текущей страницы в пути нет: её имя стоит в `h1`, а\n * путь показывает только родителей. Тогда `aria-current` не получает никто,\n * и последний пункт остаётся обычной ссылкой.\n */\n currentIndex?: number\n /** i18n-метка кнопки раскрытия схлопнутого пути. */\n expandLabel?: string\n /**\n * Схлопывать по доступной ширине, а не по числу пунктов. Путь становится\n * однострочным, середина уходит под «…» ровно настолько, насколько не влезает.\n *\n * Без пропа поведение прежнее: список переносится на вторую строку, а порог\n * задаёт `maxItems`. Вместе пропы совместимы — `maxItems` остаётся жёстким\n * потолком, ширина ужимает дальше.\n */\n autoCollapse?: boolean\n}\n\nconst props = withDefaults(defineProps<GrBreadcrumbsProps>(), {\n as: undefined,\n separator: undefined,\n size: undefined,\n maxItems: undefined,\n itemsBeforeCollapse: 1,\n itemsAfterCollapse: 1,\n ariaLabel: undefined,\n linkCurrent: false,\n currentIndex: undefined,\n expandLabel: undefined,\n autoCollapse: false,\n})\n\ndefineSlots<{\n /** Содержимое пункта целиком (иконка и подпись). */\n item?: (props: { item: GrBreadcrumbItem, index: number, isCurrent: boolean }) => unknown\n /** Разделитель между пунктами. */\n separator?: () => unknown\n /** Кнопка раскрытия схлопнутой середины. */\n ellipsis?: (props: { hiddenCount: number, expand: () => void }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrBreadcrumbs' })\nconst resolvedSeparator = useGrComponentProp('GrBreadcrumbs', 'separator', () => props.separator, '/')\n\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.breadcrumbs.label', 'Breadcrumb'))\nconst resolvedExpandLabel = computed(() => props.expandLabel ?? t('gr.breadcrumbs.expand', 'Show hidden path'))\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst listEl = ref<HTMLElement | null>(null)\nconst expanded = ref(false)\n\n/**\n * Схлопывание по ширине: сколько пунктов хвоста влезает.\n *\n * `undefined` — ещё не мерили, показываем путь целиком. Первый кадр в режиме\n * `autoCollapse` намеренно полный: ширины снимаются с него, а посчитать их до\n * рендера неоткуда.\n */\nconst fitAfter = ref<number | undefined>(undefined)\n\n/** Ширины не зависят от контейнера, поэтому живут до смены самого пути. */\nlet measured: { items: number[], separator: number } | null = null\n/**\n * Ширина кнопки «…» узнаётся только когда она отрисована, то есть уже после\n * первого схлопывания. До этого считаем её нулевой: поправка может лишь\n * ужать хвост ещё на пункт, а не вернуть спрятанное, поэтому качелей не будет.\n */\nlet ellipsisWidth = 0\n/** Ширина, на которой считали в прошлый раз: по её смене раскрытый путь сворачивается. */\nlet lastAvailable = 0\n\n// Новый путь — новая страница: раскрытая середина прошлого пути к ней отношения\n// не имеет, а измеренные ширины относятся к прошлым подписям.\nwatch(() => props.items, () => {\n expanded.value = false\n measured = null\n ellipsisWidth = 0\n fitAfter.value = undefined\n void nextTick(measure)\n})\n\nconst lastIndex = computed(() => props.items.length - 1)\n\n/**\n * Схлопывание по ширине выражается через тот же `maxItems`: раскладку считает\n * одна функция, а режимы отличаются только тем, откуда взялось число.\n */\nconst autoMaxItems = computed(() => {\n if (!props.autoCollapse || fitAfter.value === undefined) return undefined\n if (fitAfter.value >= props.items.length) return undefined\n\n return props.items.length - 1\n})\n\nconst effectiveMaxItems = computed(() => {\n const limits = [props.maxItems, autoMaxItems.value].filter((value): value is number => typeof value === 'number' && value > 0)\n\n return limits.length > 0 ? Math.min(...limits) : undefined\n})\n\nconst effectiveAfter = computed(() => (\n props.autoCollapse && fitAfter.value !== undefined ? fitAfter.value : props.itemsAfterCollapse\n))\n\nconst entries = computed(() => resolveBreadcrumbsLayout({\n items: props.items,\n maxItems: effectiveMaxItems.value,\n itemsBeforeCollapse: props.itemsBeforeCollapse,\n itemsAfterCollapse: effectiveAfter.value,\n expanded: expanded.value,\n}))\n\nconst resolvedCurrentIndex = computed(() => props.currentIndex ?? lastIndex.value)\n\nfunction isCurrent(index: number): boolean {\n return index === resolvedCurrentIndex.value\n}\n\n/**\n * Ссылкой пункт становится, только если ведёт куда-то и не выключен.\n */\nfunction isLink(item: GrBreadcrumbItem, index: number): boolean {\n if (item.disabled) return false\n if (isCurrent(index) && !props.linkCurrent) return false\n return item.href !== undefined || item.to !== undefined\n}\n\nfunction widthOf(el: Element | null | undefined): number {\n // `scrollWidth`, а не прямоугольник: у подписи стоит `truncate`, и в узком\n // контейнере прямоугольник вернул бы уже ужатую ширину, а не собственную.\n return el ? (el as HTMLElement).scrollWidth : 0\n}\n\n/**\n * Снять ширины с полного пути и пересчитать, сколько пунктов хвоста влезает.\n *\n * Замер делается один раз на путь: подписи от ширины контейнера не зависят,\n * поэтому `resize` пересчитывает только арифметику.\n */\nfunction measure(): void {\n if (!props.autoCollapse) return\n\n const list = listEl.value\n if (!list) return\n\n const ellipsis = list.querySelector('[data-gr-breadcrumbs-ellipsis-item]')\n if (ellipsis) ellipsisWidth = widthOf(ellipsis)\n\n if (!measured) {\n // Ширины снимаем только с полного пути: спрятанных пунктов в DOM нет.\n if (fitAfter.value !== undefined) return\n\n const wraps = [...list.querySelectorAll('[data-gr-breadcrumbs-item-wrap]')]\n if (wraps.length !== props.items.length) return\n\n measured = {\n items: wraps.map(widthOf),\n separator: widthOf(list.querySelector('[data-gr-breadcrumbs-separator]')),\n }\n }\n\n const available = list.clientWidth\n // jsdom и скрытый контейнер отдают 0 — решать по такой ширине нечего.\n if (available <= 0) return\n\n lastAvailable = available\n\n fitAfter.value = resolveBreadcrumbsFit({\n itemWidths: measured.items,\n separatorWidth: measured.separator,\n ellipsisWidth,\n available,\n itemsBeforeCollapse: props.itemsBeforeCollapse,\n gapWidth: listGap(list),\n })\n}\n\n/** Зазор списка в пикселях: он задан классом, а не пропом, поэтому спрашиваем стиль. */\nfunction listGap(list: HTMLElement): number {\n if (typeof getComputedStyle !== 'function') return 0\n\n return Number.parseFloat(getComputedStyle(list).columnGap) || 0\n}\n\n/**\n * Ширина кнопки «…» известна только после того, как она отрисована, то есть\n * после первого схлопывания. Поэтому решение пересчитывается ещё раз: поправка\n * либо ничего не меняет, либо ужимает хвост на пункт. Цикла нет — второй проход\n * оставляет `fitAfter` прежним, и наблюдатель больше не срабатывает.\n */\nwatch(fitAfter, () => void nextTick(measure))\n\nlet observer: ResizeObserver | null = null\n\nwatch(() => props.autoCollapse, (enabled) => {\n if (enabled) {\n void nextTick(measure)\n return\n }\n\n measured = null\n ellipsisWidth = 0\n fitAfter.value = undefined\n})\n\nonMounted(() => {\n void nextTick(measure)\n\n // На сервере и в jsdom `ResizeObserver` отсутствует — измерять там нечего.\n if (typeof ResizeObserver === 'undefined') return\n\n observer = new ResizeObserver(() => {\n // Смена ширины — новая раскладка, и раскрытая руками середина относилась к\n // прежней. Свернуть её иначе нечем: в раскрытом пути кнопки «…» нет, то есть\n // поворот телефона оставлял бы путь развёрнутым навсегда. Высота меняется от\n // самого раскрытия (путь переносится на вторую строку) — на неё не реагируем.\n if (expanded.value && listEl.value && listEl.value.clientWidth !== lastAvailable)\n expanded.value = false\n\n measure()\n })\n if (listEl.value) observer.observe(listEl.value)\n})\n\nonBeforeUnmount(() => observer?.disconnect())\n\nasync function expand(): Promise<void> {\n expanded.value = true\n\n // Кнопка исчезает вместе со схлопыванием, и фокус ушёл бы на `<body>`.\n // Переводим его на первый раскрытый пункт — там, где кнопка и стояла. Пункты\n // без ссылки держат `tabindex=\"-1\"` именно ради этого случая.\n await nextTick()\n const before = Math.max(0, Math.min(props.itemsBeforeCollapse, props.items.length))\n const revealed = rootEl.value?.querySelectorAll<HTMLElement>('[data-gr-breadcrumbs-item]')\n revealed?.[before]?.focus()\n}\n</script>\n\n<template>\n <nav\n ref=\"rootEl\"\n data-gr-breadcrumbs\n :class=\"[breadcrumbsRootClass, breadcrumbsSizeClassBySize[resolvedSize]]\"\n :aria-label=\"resolvedAriaLabel\"\n >\n <ol\n ref=\"listEl\"\n data-gr-breadcrumbs-list\n :class=\"[breadcrumbsListClass, autoCollapse && !expanded ? breadcrumbsListNowrapClass : breadcrumbsListWrapClass]\"\n >\n <template v-for=\"(entry, position) in entries\" :key=\"entry.kind === 'item' ? `item-${entry.index}` : 'ellipsis'\">\n <!--\n Разделитель — отдельный элемент списка и помечен `aria-hidden`:\n структуру пути диктору сообщает сам список, а «/» он бы читал вслух.\n -->\n <li\n v-if=\"position > 0\"\n data-gr-breadcrumbs-separator\n :class=\"breadcrumbsSeparatorClass\"\n aria-hidden=\"true\"\n >\n <slot name=\"separator\">\n {{ resolvedSeparator }}\n </slot>\n </li>\n\n <li v-if=\"entry.kind === 'ellipsis'\" data-gr-breadcrumbs-ellipsis-item class=\"min-w-0\">\n <slot name=\"ellipsis\" :hidden-count=\"entry.hiddenCount\" :expand=\"expand\">\n <button\n type=\"button\"\n data-gr-breadcrumbs-ellipsis\n data-testid=\"gr-breadcrumbs-ellipsis\"\n :class=\"breadcrumbsEllipsisClass\"\n :aria-label=\"resolvedExpandLabel\"\n @click=\"expand\"\n >\n …\n </button>\n </slot>\n </li>\n\n <li v-else data-gr-breadcrumbs-item-wrap :class=\"breadcrumbsItemWrapClass\">\n <GrLink\n v-if=\"isLink(entry.item, entry.index)\"\n :key=\"`link-${entry.index}`\"\n data-gr-breadcrumbs-item\n :as=\"as\"\n :href=\"entry.item.href\"\n :to=\"entry.item.to\"\n :size=\"resolvedSize\"\n :variant=\"isCurrent(entry.index) ? 'default' : 'muted'\"\n :aria-label=\"entry.item.ariaLabel\"\n :aria-current=\"isCurrent(entry.index) ? 'page' : undefined\"\n >\n <slot name=\"item\" :item=\"entry.item\" :index=\"entry.index\" :is-current=\"isCurrent(entry.index)\">\n <component\n :is=\"iconTag(entry.item.icon)\"\n v-if=\"entry.item.icon\"\n :class=\"[breadcrumbsItemIconClass, iconClass(entry.item.icon)]\"\n aria-hidden=\"true\"\n />\n <span :class=\"breadcrumbsLabelClass\">{{ entry.item.label }}</span>\n </slot>\n </GrLink>\n\n <!--\n Текущая страница и выключенный пункт — не ссылки. `aria-current`\n остаётся на текущем: именно он отвечает на вопрос «где я».\n -->\n <span\n v-else\n data-gr-breadcrumbs-item\n :class=\"breadcrumbsCurrentClass\"\n :aria-label=\"entry.item.ariaLabel\"\n :aria-current=\"isCurrent(entry.index) ? 'page' : undefined\"\n :aria-disabled=\"entry.item.disabled ? 'true' : undefined\"\n tabindex=\"-1\"\n >\n <slot name=\"item\" :item=\"entry.item\" :index=\"entry.index\" :is-current=\"isCurrent(entry.index)\">\n <component\n :is=\"iconTag(entry.item.icon)\"\n v-if=\"entry.item.icon\"\n :class=\"[breadcrumbsItemIconClass, iconClass(entry.item.icon)]\"\n aria-hidden=\"true\"\n />\n <span :class=\"breadcrumbsLabelClass\">{{ entry.item.label }}</span>\n </slot>\n </span>\n </li>\n </template>\n </ol>\n </nav>\n</template>\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrDelta-04j9WmEY.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 GrValue from '../GrValue'\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 deltaValueStyle,\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\" :style=\"deltaValueStyle\">\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 <!-- Запись величины рисует `GrValue`: приписки оформляются там же, где у\n остальных величин пакета, и настраиваются теми же токенами. Знак идёт\n в `#lead` — он обязан встать перед валютой, а не после неё. -->\n <GrValue\n :value=\"formatted\"\n :prefix=\"isEmpty ? undefined : prefix\"\n :suffix=\"isEmpty ? undefined : suffix\"\n >\n <template v-if=\"sign\" #lead>\n <span data-gr-delta-sign>{{ sign }}</span>\n </template>\n </GrValue>\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport { formatNumberToParts, splitLeadingSign } from '../../internal/granularityFormat'\nimport GrValue from '../GrValue'\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 deltaValueStyle,\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\" :style=\"deltaValueStyle\">\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 <!-- Запись величины рисует `GrValue`: приписки оформляются там же, где у\n остальных величин пакета, и настраиваются теми же токенами. Знак идёт\n в `#lead` — он обязан встать перед валютой, а не после неё. -->\n <GrValue\n :value=\"formatted\"\n :prefix=\"isEmpty ? undefined : prefix\"\n :suffix=\"isEmpty ? undefined : suffix\"\n >\n <template v-if=\"sign\" #lead>\n <span data-gr-delta-sign>{{ sign }}</span>\n </template>\n </GrValue>\n </span>\n</template>\n"],"mappings":"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"}
|