GrTimePicker

Package: @feugene/granularity-chronocompanionGroup: misc

Machine-translated from the Russian original, not yet reviewed. Read the original

When to take it

  • a time without a date — the start of a shift, opening hours, a reminder: the day here either does not matter or is set separately;
  • the time is more convenient to typeeditable gives typing from the keyboard, and the parsing goes by the locale, the 12-hour form included where it is customary;
  • seconds are neededenableSeconds adds a third column;
  • the step is not a minuteminuteStep builds a column with the required step, and “14:07” can no longer be entered where appointments go by half-hours.

When to take something else

NeedTake
A date together with a timeGrDateTimePicker
A date onlyGrDatePicker
A “from — to” period by datesGrDateRangePicker
Enter a duration rather than a moment on the clockGrNumberInput
Show a durationGrDuration
Several ready slots to choose fromGrSelect

A duration is not a time. “An hour and a half” is a number with a unit rather than a moment: the columns of hours and minutes lie about it, because 90 minutes cannot be assembled in them. It is entered as a number and shown with GrDuration.

The panel does not close on a selection

The hour, the minute and the period are three separate decisions, and closing after the first of them would mean the user chose a time they did not choose. That is precisely the reason the time panel behaves unlike a calendar.

The value stays a `Date`

Even when the date does not matter, a full moment goes outside — with the date from today or from the current value. There is no separate “time of day” type in the model: it would require arithmetic of its own and an adapter of its own for the sake of one component. If a '14:30' string is needed, that is a valueAdapter on the consumer’s side.

Install

npm i @feugene/granularity-chrono

Import

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

API

The API for this component has not been generated yet: the showcase generator only covers the core so far. Until it does, the reference lives in the package documentation.

Examples 3

Basic

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

// `GrTimePicker` подставляется авто-импортом (`unplugin-vue-components`).
const value = ref<Date | null>(new Date(2026, 7, 12, 9, 30))

// Границы учитывают только время суток: рабочий день с 09:00 до 18:00.
const from = new Date(2026, 7, 12, 9, 0)
const to = new Date(2026, 7, 12, 18, 0)
</script>

<template>
  <div class="grid max-w-[320px] gap-4">
    <GrTimePicker
      v-model="value"
      :min="from"
      :max="to"
      :minute-step="15"
      :use12-hours="false"
      clearable
      placeholder="Pick a time"
      aria-label="Meeting time"
    />

    <p class="showcase-demo-text text-sm">
      <span class="opacity-70">value=</span>
      <code>{{ value ? value.toTimeString().slice(0, 5) : '—' }}</code>
    </p>
  </div>
</template>

Footer

Footerdepends on the showcase environment
<script setup lang="ts">
import { computed, ref } from 'vue'
import IconClock from '~icons/lucide/clock-arrow-up'

// `GrTimePicker`, `GrButton` и `GrSwitch` подставляются авто-импортом.
import { ceilToStep, toPlainTime, useChronoNow } from '@feugene/granularity-chrono'

/**
 * Подвал панели раздаёт выбор внутрь.
 *
 * Демо намеренно даёт переключить `max` на «сейчас плюс две минуты»: на нём
 * видно, что граница считается **после** округления к шагу, и кнопка гаснет,
 * а не молча ничего не делает.
 */
const MINUTE_STEP = 15

const value = ref<Date | null>(null)
const tight = ref(false)

/**
 * Общие часы пакета, а не свой `setInterval`: подпись на кнопке обязана
 * оставаться правдой, но будить вкладку ради этого раз в секунду незачем —
 * такт здесь минутный.
 */
const now = useChronoNow(60_000)

/** Ровно то, что положит кнопка: «сейчас», поставленное на сетку шага. */
const target = computed(() => ceilToStep(toPlainTime(now.value), MINUTE_STEP * 60))

const targetLabel = computed(() => (
  `${String(target.value.h).padStart(2, '0')}:${String(target.value.min).padStart(2, '0')}`
))

/**
 * При включённом переключателе граница стоит на две минуты позже текущего
 * момента — то есть заведомо раньше следующей отметки пятнадцатиминутной сетки.
 */
const max = computed(() => (tight.value ? new Date(now.value.getTime() + 2 * 60_000) : undefined))

const maxLabel = computed(() => (max.value
  ? `${String(max.value.getHours()).padStart(2, '0')}:${String(max.value.getMinutes()).padStart(2, '0')}`
  : null))
</script>

<template>
  <div class="grid gap-4 justify-items-start">
    <GrSwitch v-model="tight" size="sm">
      Ограничить <code>max</code> двумя минутами вперёд
    </GrSwitch>

    <GrTimePicker
      v-model="value"
      :minute-step="MINUTE_STEP"
      :max="max"
      aria-label="Start time"
    >
      <template #footer="{ select, canSelect }">
        <div class="flex w-full items-center justify-between gap-3">
          <!--
            Причина отказа рядом с кнопкой, а не вместо неё: выключенный
            контрол без объяснения читается как поломка.
          -->
          <span class="text-xs leading-tight text-[var(--gr-muted-fg)]">
            <template v-if="canSelect(now)">
              ближайшая отметка
              <strong class="font-600 text-[var(--gr-fg)]">{{ targetLabel }}</strong>
            </template>
            <template v-else>
              {{ targetLabel }} позже, чем {{ maxLabel }}
            </template>
          </span>

          <GrButton
            size="sm"
            variant="outline"
            :disabled="!canSelect(now)"
            @click="select(now)"
          >
            <template #prefix>
              <IconClock />
            </template>
            Сейчас
          </GrButton>
        </div>
      </template>
    </GrTimePicker>

    <p class="showcase-demo-text text-sm opacity-70">
      Шаг — 15 минут, и «сейчас» встаёт на <strong>следующую</strong> отметку: 14:37 даёт 14:45, а не
      14:30. Округление вверх, потому что время в пикере почти всегда значит «начиная с этого
      момента» — запись, бронь, напоминание, — и округлённое вниз уже прошло. Подпись слева называет
      будущее значение до нажатия: у кнопки, которая молча меняет время на непредсказуемое, нет
      способа сказать «не то».
    </p>

    <p class="showcase-demo-text text-sm opacity-70">
      Включите ограничение: граница окажется раньше следующей отметки, и кнопка погаснет, а подпись
      объяснит почему. Порядок здесь и есть предмет: сначала время встаёт на сетку, и только потом
      проверяются границы — проверка до округления пропустила бы кнопку, которая ничего не делает.
    </p>
  </div>
</template>

Twelve Hour

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

// `GrTimePicker` подставляется авто-импортом (`unplugin-vue-components`).
const value = ref<string | null>('2026-08-12T15:30:45')
</script>

<template>
  <div class="grid max-w-[320px] gap-4">
    <!-- 12/24 приходит из локали, но проп перебивает её; секунды добавляют
         третью колонку и попадают в показ. -->
    <GrTimePicker
      v-model="value"
      value-adapter="isoDateTime"
      use12-hours
      enable-seconds
      :second-step="15"
      clearable
      aria-label="Start time"
    />

    <p class="showcase-demo-text text-sm">
      <span class="opacity-70">value=</span><code>{{ value ?? '—' }}</code>
    </p>
  </div>
</template>

Component documentationAll components