GrTimePicker
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 type —
editablegives typing from the keyboard, and the parsing goes by the locale, the 12-hour form included where it is customary; - seconds are needed —
enableSecondsadds a third column; - the step is not a minute —
minuteStepbuilds 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
| Need | Take |
|---|---|
| A date together with a time | GrDateTimePicker |
| A date only | GrDatePicker |
| A “from — to” period by dates | GrDateRangePicker |
| Enter a duration rather than a moment on the clock | GrNumberInput |
| Show a duration | GrDuration |
| Several ready slots to choose from | GrSelect |
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-chronoImport
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
<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
<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
<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>