GrDelta
Берут, когда число со знаком стоит в предложении.
Когда брать
- число со знаком стоит в предложении — в описании операции, в ячейке таблицы, в подписи под графиком;
- знак решает цвет — рост, падение и «не изменилось» видно до чтения самой величины;
- рост бывает плохим —
polarity="negative-good"для себестоимости, времени отклика и оттока; - величина может отсутствовать — «нет данных» и «ноль» рисуются по-разному.
Когда взять другое
| Нужно | Берите |
|---|---|
| Крупный показатель плиткой, с подписью и динамикой | GrStatistic |
| Число как статус или метка | GrBadge |
| Доля от целого полосой или кольцом | GrProgressBar / GrProgressCircle |
| Ряд значений во времени | GrSparkline |
| Пара «характеристика → значение» | GrDescriptionList |
Ноль нейтрален, `null` — это не ноль
Два состояния, которые двумя цветами не выражаются, и оба уже приводили к ошибкам в приложениях:
- ноль — «не изменилось». Ни успех, ни ошибка: тон нейтральный при любой
polarity. Условие видаvalue < 0 ? danger : successкрасит нулевое движение зелёным, как поступление; null— величины нет вовсе. Печатается прочерком (emptyText), без тона, без стрелки и без приписок:$—читается как «ноль долларов».
Выбор тона живёт отдельной чистой функцией и покрыт тестами без монтирования —
именно этот switch расходился копиями по приложению.
Функция публичная: deltaTone(value, polarity) и deltaDirection(value)
отдаются из пакета. Они нужны там, где разметка дельты не подходит, а правило
то же — tone у GrStatistic и GrBadge в
плитках отчёта. Переписанное в приложении, оно расходится с этим при первой же
правке — что и произошло с нулём, покрашенным в зелёный как поступление.
Полярность инвертирует тон, но не знак
<GrDelta :value="-15" suffix="%" polarity="negative-good" />
Себестоимость упала на 15 % — это успех, и величина зелёная. Но она всё ещё
−15 %: знак принадлежит числу, а не оценке. Стрелка тоже идёт за знаком —
вниз, потому что величина уменьшилась.
polarity="none" снимает тон совсем: у сальдо и смещения знак ничего не
говорит о качестве.
Знак ставит `Intl`, а не склейка строк
showSign включает signDisplay: 'exceptZero' в форматировании, а не
дописывает '+' к готовой строке. Разница не стилистическая: склейка уже
ломала GrStatistic — строка '+1,234' перестаёт быть числом, и разряды
теряются на следующем шаге.
Локаль берётся из i18n-адаптера, если не задана пропом.
Знак стоит перед валютой
Вместе с prefix знак уезжает в отдельный узел перед припиской: +$0,0280,
а не $+0,0280. Знак относится к величине целиком, а не к числу после символа
валюты, и второй порядок не принят ни в одной типографской традиции.
Инвариант выше при этом не ослаблен: знак по-прежнему ставит Intl, компонент
лишь вынимает его из formatToParts — строка не склеивается и разряды не
пересобираются. Без prefix выносить некуда, и разметка остаётся прежней.
Настраивать положение знака нечем намеренно. Отдельная тонкость — RTL: там знаку предшествует невидимая метка направления, которой он и разворачивается относительно цифр; она уезжает вместе со знаком, потому что управляет именно им.
Сами приписки рисует GrValue — общий примитив с
GrStatistic. Оттуда же их оформление и токены --gr-value-*: валюта слева
набирается как число, единица справа приглушается. Валюта справа (+1 000 ₽)
получается сменой дефолта суффикса, рецепт — на странице примитива.
Кегль приходит из строки
Ступень по умолчанию (md) font-size не задаёт вовсе: величина набирается
кеглем той строки, в которой стоит. Внутри заголовка она крупная, в подписи под
графиком мелкая, в ячейке таблицы — ровно как остальной текст ячейки, и всё это
без единого пропа.
<h2 class="text-[length:var(--gr-text-3xl)]">
Выручка за март <GrDelta :value="8.4" :precision="1" suffix="%" show-arrow />
</h2>
Края лестницы остаются явными и берут контрольную шкалу — xs 12 px, sm 13 px,
lg 16 px. Они нужны в другом случае: когда величина стоит не в предложении, а
в ряду с контролами, и обязана совпасть с ними, а не с окружающим текстом.
Стрелка задана в em и растёт вместе с числом — своей ступени у неё нет.
Переопределяя кегль снаружи, переопределяйте пару: межстрочный
компонент не трогает, и половинчатая правка оставит строку с чужим интервалом.
Стрелка декоративна
showArrow рисует направление, но помечает иконку aria-hidden: направление
уже объявлено знаком, и дублировать его для скринридера незачем. По умолчанию
стрелки нет — в строке текста она чаще шумит, чем помогает.
Границы
Дельту компонент не считает: принимает готовую. «К чему сравнивать» — период, базовое значение, фильтры — знает приложение, и втаскивать это в презентационный компонент значит тащить туда же его модель данных. Проценты, сравнение периодов и спарклайн — тоже снаружи.
Playground 8
Загружается…
<GrDelta />Установка
npm i @feugene/granularityИмпорт
import { GrDelta } from '@feugene/granularity/components/GrDelta'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
emptyText | string | undefined | undefined | Чем печатать отсутствующую величину. |
locale | string | undefined | undefined | Локаль форматирования. Не задана — локаль i18n-адаптера. |
precision | number | undefined | undefined | Знаков после запятой. Не задано — как отдаст локаль. |
prefix | string | undefined | undefined | Приписка перед числом: валюта, единица. |
suffix | string | undefined | undefined | Приписка после числа: `%`, `мс`. |
polarity | GrDeltaPolarity | undefined | undefined | Что считать хорошим. Для выручки рост — успех, для себестоимости и времени отклика — наоборот; без этого компонент врал бы в половине случаев. |
showSign | boolean | undefined | undefined | Ставить `+` у положительных. Минус подставляет `Intl` сам. |
showArrow | boolean | undefined | undefined | Стрелка направления. Декоративна: направление уже в знаке. |
valueобязательный | number | null | — | Величина. `null` — её нет; это не ноль. |
Примеры 3
Величина со знаком внутри предложения
Знак, тон и приписки для величины, стоящей в строке текста. Ноль нейтрален, null печатается прочерком без тона — «нет данных» и «ноль» это разные утверждения.
Balance change: +$1 284,50
Margin: -$12,50
Unchanged: $0,00
Not measured: —
<script setup lang="ts">
import { GrDelta } from '@feugene/granularity'
</script>
<template>
<div class="grid gap-2 text-sm">
<p>
Balance change: <GrDelta :value="1284.5" :precision="2" prefix="$" />
</p>
<p>
Margin: <GrDelta :value="-12.5" :precision="2" prefix="$" />
</p>
<!-- Ноль нейтрален: «не изменилось» — третье состояние, и двумя цветами
его не выразить. -->
<p>
Unchanged: <GrDelta :value="0" :precision="2" prefix="$" />
</p>
<!-- `null` — величины нет вовсе. Это не ноль, поэтому ни тона, ни приписок. -->
<p>
Not measured: <GrDelta :value="null" prefix="$" />
</p>
</div>
</template>Знак ставит Intl через signDisplay, а не склейка строк: '+' + value превращает число в строку и теряет разряды.
Полярность: когда рост — это плохо
Для выручки рост — успех, для оттока и времени отклика — наоборот. polarity инвертирует тон, оставляя знак и направление стрелки за самой величиной.
Revenue: +8,4% — polarity="positive-good"
Churn: +2,1% — polarity="negative-good"
Response time: -15,0% — polarity="negative-good"
Balance offset: -3,2% — polarity="none"
<script setup lang="ts">
import { GrDelta } from '@feugene/granularity'
const rows = [
{ metric: 'Revenue', value: 8.4, polarity: 'positive-good' as const },
{ metric: 'Churn', value: 2.1, polarity: 'negative-good' as const },
{ metric: 'Response time', value: -15, polarity: 'negative-good' as const },
{ metric: 'Balance offset', value: -3.2, polarity: 'none' as const },
]
</script>
<template>
<!--
Полярность инвертирует тон, но не знак: «−15 %» времени отклика зелёное,
потому что стало быстрее, — и всё ещё минус. Без этого компонент врал бы
в половине случаев.
-->
<div class="grid gap-2 text-sm">
<p v-for="row in rows" :key="row.metric">
{{ row.metric }}:
<GrDelta :value="row.value" :precision="1" suffix="%" :polarity="row.polarity" show-arrow />
<!-- Не `opacity-*`: прозрачность разбавляет выверенный на AA токен и роняет контраст. -->
<span class="text-[var(--gr-muted-fg)]"> — polarity="{{ row.polarity }}"</span>
</p>
</div>
</template>Величина набирается кеглем своей строки
Одна и та же разметка в заголовке, в подзаголовке и в подписи: size не задан нигде, кегль приходит от строки. Стрелка и суффикс растут вместе с числом.
<script setup lang="ts">
import { GrDelta } from '@feugene/granularity'
</script>
<template>
<!--
Разметка величины во всех трёх строках одна и та же — `size` не задан
нигде. Кегль приходит от строки, поэтому стрелка и суффикс растут вместе
с числом, а не остаются 14-пиксельными внутри заголовка.
-->
<div class="grid gap-4">
<div class="text-[length:var(--gr-text-3xl)] leading-[var(--gr-leading-3xl)] font-600">
Выручка за март
<GrDelta :value="8.4" :precision="1" suffix="%" show-arrow />
</div>
<div class="text-[length:var(--gr-text-xl)] leading-[var(--gr-leading-xl)]">
Средний чек
<GrDelta :value="-3.2" :precision="1" suffix="%" show-arrow />
</div>
<div class="text-[length:var(--gr-control-text-sm)]">
Возвраты
<GrDelta :value="1.6" :precision="1" suffix="%" polarity="negative-good" show-arrow />
</div>
<!--
Явная ступень нужна там, где величина стоит не в предложении, а в ряду
с контролами: тогда она обязана совпасть с ними, а не с текстом вокруг.
-->
<div class="flex items-center gap-2 text-[length:var(--gr-text-xl)]">
<span>В ряду с контролами:</span>
<GrDelta :value="8.4" :precision="1" suffix="%" size="sm" />
</div>
</div>
</template>Явная ступень (size="sm") нужна в обратном случае — когда величина стоит не в предложении, а в ряду с контролами и обязана совпасть с ними, а не с текстом вокруг.