GrDuration

Пакет: @feugene/granularity-chronoспутникГруппа: Прочее

Берут, когда сколько заняло.

Когда брать

  • сколько заняло — длительность звонка, время сборки, простой оборудования: «2 ч 30 мин» читается быстрее, чем «9000»;
  • сколько идёт прямо сейчас — передайте момент начала одной датой, и метка станет живой: таймер общий на всё приложение и снимается на скрытой вкладке;
  • промежуток между двумя моментами — пара дат считается сама, вычитать миллисекунды руками не нужно;
  • значение уезжает в разметкуdatetime заполняется точной длиной в ISO 8601, даже когда текст сокращён до двух единиц.

Когда взять другое

НужноБерите
Показать, давно ли это былоGrRelativeTime
Выбрать момент, а не показать длинуGrDatePicker / GrTimePicker
Ввести длительность, а не показатьGrNumberInput
Показать долю прошедшего от целогоGrProgressBar

Единиц четыре, и крупнее не будет

Сутки, часы, минуты, секунды. Месяца и года здесь нет намеренно: они календарные — в феврале и в июле разной длины, — и вывести их из числа секунд нельзя, не соврав. Разрыв, который правда меряется календарём, показывает GrRelativeTime, а считает differenceInMonths.

Потолок, а не квота

maxUnits ограничивает число единиц сверху и ничего не добирает: ровно два часа это «2 ч», а не «2 ч 0 мин». Младшее при этом отбрасывается, а не округляется — «2 ч 59 мин 30 с» остаётся «2 ч 59 мин». Округление подняло бы показ выше реально прошедшего времени, а этим значением обычно меряют именно его.

Ноль — единственное исключение из отбрасывания: «0 с» печатается, потому что пустая строка читается как «нет данных», а ноль означает другое.

Живая метка — только у формы с одной датой

Число и пара дат задают длину целиком, тикать в них нечему. Момент начала — другое дело: до «сейчас» время идёт, и метка пересчитывается сама. Такт выбирается по младшей показанной единице, поэтому метка в часах не будит вкладку каждую секунду.

Живой показ выведен из часов, а значит серверный рендер и клиентский разойдутся. Компонент помечает это data-allow-mismatch, и убирается пометка не флагом, а данными: передайте base — рендер станет детерминированным.

Границы

Обратного отсчёта здесь нет: «осталось 2 ч 30 мин» — это та же длина, но дедлайн, ноль и переход в отрицательные значения принадлежат сценарию, а не метке. Считайте остаток сами и передавайте числом.

Установка

npm i @feugene/granularity-chrono

Импорт

import { GrDuration } from '@feugene/granularity-chrono/components/GrDuration'

API

API этого компонента ещё не посчитан: генератор витрины пока обходит только ядро. Пока его нет, справочник — в документации пакета.

Примеры 1

Basic

Basic
<script setup lang="ts">
import { ref } from 'vue'

/**
 * Три формы значения — три разных вопроса. Число это готовая длина, пара дат —
 * промежуток между моментами, одна дата — время, которое идёт прямо сейчас.
 */
const started = ref(new Date())

const rows = [
  { title: 'Длительность звонка', value: 9000 },
  { title: 'Сборка заняла', value: 10_770 },
]

const meeting: [Date, Date] = [
  new Date(2026, 7, 12, 9, 0),
  new Date(2026, 7, 12, 11, 30),
]
</script>

<template>
  <div class="grid gap-3">
    <div v-for="row in rows" :key="row.title" class="flex items-baseline justify-between gap-6 text-sm">
      <span class="opacity-70">{{ row.title }}</span>
      <GrDuration :value="row.value" />
    </div>

    <div class="flex items-baseline justify-between gap-6 text-sm">
      <span class="opacity-70">Совещание</span>
      <GrDuration :value="meeting" />
    </div>

    <div class="flex items-baseline justify-between gap-6 text-sm">
      <span class="opacity-70">Страница открыта</span>
      <GrDuration :value="started" />
    </div>

    <p class="showcase-demo-text text-sm opacity-70">
      «Сборка заняла» показывает <strong>2 ч 59 мин</strong>, а не «3 ч»: младшее
      отбрасывается, а не округляется. Последняя строка тикает сама — такт
      выбирается по младшей показанной единице, и таймер в приложении один на всех.
    </p>
  </div>
</template>

Документация компонентаВсе компоненты