GrTimePicker
Берут, когда время без даты.
Когда брать
- время без даты — начало смены, часы работы, напоминание: день здесь либо не важен, либо задан отдельно;
- время удобнее набрать —
editableдаёт ввод с клавиатуры, разбор идёт по локали, включая 12-часовой вид там, где он принят; - нужны секунды —
enableSecondsдобавляет третью колонку; - шаг не минутный —
minuteStepстроит колонку с нужным шагом, и «14:07» там, где приём идёт по получасам, набрать уже нельзя.
Когда взять другое
| Нужно | Берите |
|---|---|
| Дата вместе со временем | GrDateTimePicker |
| Только дата | GrDatePicker |
| Период «с — по» по датам | GrDateRangePicker |
| Ввести длительность, а не момент на часах | GrNumberInput |
| Показать длительность | GrDuration |
| Несколько готовых слотов на выбор | GrSelect |
Длительность — не время. «Полтора часа» это число с единицей, а не момент:
колонки часов и минут для неё врут, потому что 90 минут в них не набираются.
Вводят её числом, а показывают GrDuration.
Панель не закрывается по выбору
Час, минута и период — три отдельных решения, и закрытие после первого из них означало бы, что пользователь выбрал время, которого он не выбирал. Это и есть причина, по которой панель времени ведёт себя не как календарь.
Подвал раздаёт выбор внутрь
Слот footer получает select, canSelect и close — та же тройка, что у пикеров дат: подвал у
всех пикеров пакета устроен одинаково.
<GrTimePicker v-model="time" :minute-step="15">
<template #footer="{ select, canSelect }">
<GrButton size="sm" variant="ghost" :disabled="!canSelect(new Date())" @click="select(new Date())">
Сейчас
</GrButton>
</template>
</GrTimePicker>
Время встаёт на сетку шага округлением вверх. 14:37 при шаге в 15 минут даёт 14:45, а не 14:30: время в пикере почти всегда значит «начиная с этого момента» — запись, бронь, напоминание, — и округлённое вниз уже прошло. Этим «сейчас» отличается от колонок, где показ существующего значения округляется вниз: там задача обратная — не выдумать отметку, которой в колонке нет.
Побеждает самая крупная объявленная ступень, мелкие обнуляются: при minuteStep: 15 получится
14:45:00, а не 14:45:37. Секунды выключены — их не будет и в значении, иначе в модель уехало бы то,
чего на экране не было.
Границы считаются после округления, а не до. При max в 14:40 и шаге 15 минут «сейчас» в 14:37
округляется до 14:45 — уже за границей, и canSelect отдаёт false. Кнопка приходит выключенной, а
не молча ничего не делает: проверка до округления пропустила бы её.
readonly и disabled запрещают выбор целиком — снаружи этих правил не видно, поэтому подвал и
спрашивает canSelect, а не считает сам.
Значение остаётся `Date`
Даже когда дата не важна, наружу уходит полноценный момент — с датой из today
или из текущего значения. Отдельного типа «время суток» в модели нет: он
потребовал бы своей арифметики и своего адаптера ради одного компонента.
Нужна строка '14:30' — это valueAdapter на стороне потребителя.
Установка
npm i @feugene/granularity-chronoИмпорт
import { GrTimePicker } from '@feugene/granularity-chrono/components/GrTimePicker'API
API этого компонента ещё не посчитан: генератор витрины пока обходит только ядро. Пока его нет, справочник — в документации пакета.
Примеры 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>