Time Picker
Vue date-timeStandalone time input — hours, minutes, optional seconds, and 12h/24h modes. Pairs with Date Picker for full datetime entry.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/time-picker.json$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/time-picker.json$ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/time-picker.json$ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/time-picker.json
Or with the named registry:
npx shadcn-vue@latest add @uipkge/time-picker
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
modelValue Time value. HH:mm or HH:mm:ss depending on format. Empty = no selection. | string | '' | optional |
use24Hour | boolean | false | optional |
use12Hours | boolean | false | optional |
minuteStep | number | 5 | optional |
hourStep | number | 1 | optional |
secondStep | number | 1 | optional |
minTime 24h HH:mm or HH:mm:ss. | string | — | optional |
maxTime 24h HH:mm or HH:mm:ss. | string | — | optional |
format | TimeFormat | 'HH:mm' | optional |
disabledHours | () => number[] | — | optional |
disabledMinutes | (selectedHour: number) => number[] | — | optional |
disabledSeconds | (selectedHour: number, selectedMinute: number) => number[] | — | optional |
hideDisabledOptions | boolean | false | optional |
visible Auto-scroll active rows into view when this becomes true. | boolean | true | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
TimeParts interface TimeParts {
hour24: number
minute: number
second: number
} TimePreset interface TimePreset {
label: string
value: string
} TimeRangePreset interface TimeRangePreset {
label: string
value: [string, string]
} Dependencies
Used by
Files (4)
-
app/components/ui/time-picker/TimeColumns.vue 10.3 kB
<script setup lang="ts"> import { computed, nextTick, ref, watch } from 'vue' import { ScrollArea } from '@/components/ui/scroll-area' export interface TimeParts { hour24: number minute: number second: number } export type TimeFormat = 'HH:mm' | 'HH:mm:ss' | 'hh:mm A' interface Props { /** Time value. HH:mm or HH:mm:ss depending on format. Empty = no selection. */ modelValue?: string use24Hour?: boolean use12Hours?: boolean minuteStep?: number hourStep?: number secondStep?: number /** 24h HH:mm or HH:mm:ss. */ minTime?: string /** 24h HH:mm or HH:mm:ss. */ maxTime?: string format?: TimeFormat disabledHours?: () => number[] disabledMinutes?: (selectedHour: number) => number[] disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[] hideDisabledOptions?: boolean /** Auto-scroll active rows into view when this becomes true. */ visible?: boolean } const props = withDefaults(defineProps<Props>(), { modelValue: '', use24Hour: false, use12Hours: false, minuteStep: 5, hourStep: 1, secondStep: 1, format: 'HH:mm', hideDisabledOptions: false, visible: true, }) const emits = defineEmits<{ 'update:modelValue': [value: string] }>() function parse(v: string): TimeParts | null { if (!v) return null const m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec(v.trim()) if (!m) return null const h = Number(m[1]) const min = Number(m[2]) const s = m[3] ? Number(m[3]) : 0 if ( Number.isNaN(h) || Number.isNaN(min) || Number.isNaN(s) || h < 0 || h > 23 || min < 0 || min > 59 || s < 0 || s > 59 ) return null return { hour24: h, minute: min, second: s } } function format24(p: TimeParts) { if (props.format === 'HH:mm:ss') { return `${String(p.hour24).padStart(2, '0')}:${String(p.minute).padStart(2, '0')}:${String(p.second).padStart(2, '0')}` } return `${String(p.hour24).padStart(2, '0')}:${String(p.minute).padStart(2, '0')}` } function toMinutes(p: TimeParts) { return p.hour24 * 60 + p.minute + p.second / 60 } const parts = ref<TimeParts | null>(parse(props.modelValue)) watch( () => props.modelValue, (v) => { parts.value = parse(v) }, ) const showSeconds = computed(() => props.format === 'HH:mm:ss') const effective12Hour = computed(() => { if (props.format === 'hh:mm A') return true if (props.use12Hours) return true return !props.use24Hour }) const hour12 = computed(() => { if (!parts.value) return null const h = parts.value.hour24 % 12 return h === 0 ? 12 : h }) const period = computed(() => (parts.value && parts.value.hour24 >= 12 ? 'PM' : 'AM')) const minBound = computed(() => parse(props.minTime ?? '') ?? null) const maxBound = computed(() => parse(props.maxTime ?? '') ?? null) function withinBounds(p: TimeParts) { const m = toMinutes(p) if (minBound.value && m < toMinutes(minBound.value)) return false if (maxBound.value && m > toMinutes(maxBound.value)) return false return true } const disabledHoursSet = computed(() => { const arr = props.disabledHours ? props.disabledHours() : [] return new Set(arr) }) const disabledMinutesSet = computed(() => { const h = parts.value?.hour24 ?? 0 const arr = props.disabledMinutes ? props.disabledMinutes(h) : [] return new Set(arr) }) const disabledSecondsSet = computed(() => { const h = parts.value?.hour24 ?? 0 const m = parts.value?.minute ?? 0 const arr = props.disabledSeconds ? props.disabledSeconds(h, m) : [] return new Set(arr) }) function isHourDisabledItem(h: number) { if (disabledHoursSet.value.has(h)) return true const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 } if (effective12Hour.value) { const isPM = period.value === 'PM' return !withinBounds({ hour24: (h % 12) + (isPM ? 12 : 0), minute: cur.minute, second: cur.second }) } return !withinBounds({ hour24: h, minute: cur.minute, second: cur.second }) } function isMinuteDisabledItem(m: number) { if (disabledMinutesSet.value.has(m)) return true const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 } return !withinBounds({ hour24: cur.hour24, minute: m, second: cur.second }) } function isSecondDisabledItem(s: number) { if (disabledSecondsSet.value.has(s)) return true const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 } return !withinBounds({ hour24: cur.hour24, minute: cur.minute, second: s }) } const hours12List = computed(() => { const step = Math.max(1, props.hourStep) const list = Array.from({ length: 12 }, (_, i) => i + 1).filter((h) => (h - 1) % step === 0) if (!props.hideDisabledOptions) return list return list.filter((h) => !isHourDisabledItem(h)) }) const hours24List = computed(() => { const step = Math.max(1, props.hourStep) const list = Array.from({ length: 24 }, (_, i) => i).filter((h) => h % step === 0) if (!props.hideDisabledOptions) return list return list.filter((h) => !isHourDisabledItem(h)) }) const minutesList = computed(() => { const step = Math.max(1, Math.min(60, props.minuteStep)) const list = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step) if (!props.hideDisabledOptions) return list return list.filter((m) => !isMinuteDisabledItem(m)) }) const secondsList = computed(() => { const step = Math.max(1, Math.min(60, props.secondStep)) const list = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step) if (!props.hideDisabledOptions) return list return list.filter((s) => !isSecondDisabledItem(s)) }) function commit(p: TimeParts) { if (!withinBounds(p)) return parts.value = p emits('update:modelValue', format24(p)) } function pickHour12(h: number) { const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 } const isPM = period.value === 'PM' commit({ hour24: (h % 12) + (isPM ? 12 : 0), minute: cur.minute, second: cur.second }) } function pickHour24(h: number) { const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 } commit({ hour24: h, minute: cur.minute, second: cur.second }) } function pickMinute(m: number) { const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 } commit({ hour24: cur.hour24, minute: m, second: cur.second }) } function pickSecond(s: number) { const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 } commit({ hour24: cur.hour24, minute: cur.minute, second: s }) } function pickPeriod(p: 'AM' | 'PM') { const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 } const h12 = cur.hour24 % 12 commit({ hour24: h12 + (p === 'PM' ? 12 : 0), minute: cur.minute, second: cur.second }) } const hourCol = ref<HTMLElement | null>(null) const minCol = ref<HTMLElement | null>(null) const secCol = ref<HTMLElement | null>(null) watch( () => props.visible, (v) => { if (!v) return nextTick(() => { hourCol.value?.querySelector('[data-active="true"]')?.scrollIntoView({ block: 'center' }) minCol.value?.querySelector('[data-active="true"]')?.scrollIntoView({ block: 'center' }) secCol.value?.querySelector('[data-active="true"]')?.scrollIntoView({ block: 'center' }) }) }, { immediate: true }, ) function isHourActive(h: number) { if (!parts.value) return false return effective12Hour.value ? hour12.value === h : parts.value.hour24 === h } function isHourDisabled(h: number) { if (!props.hideDisabledOptions) return isHourDisabledItem(h) return false } function isMinuteDisabled(m: number) { if (!props.hideDisabledOptions) return isMinuteDisabledItem(m) return false } function isSecondDisabled(s: number) { if (!props.hideDisabledOptions) return isSecondDisabledItem(s) return false } </script> <template> <div class="flex divide-x" data-uipkge data-slot="time-columns"> <ScrollArea ref="hourCol" class="h-56"> <div class="flex w-14 flex-col p-1"> <button v-for="h in effective12Hour ? hours12List : hours24List" :key="h" type="button" :data-active="isHourActive(h)" :disabled="isHourDisabled(h)" class="hover:bg-accent focus-visible:ring-ring rounded px-2 py-1 text-center text-sm tabular-nums transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:opacity-30 disabled:hover:bg-transparent" :class="isHourActive(h) ? 'bg-primary text-primary-foreground hover:bg-primary' : ''" @click="effective12Hour ? pickHour12(h) : pickHour24(h)" > {{ String(h).padStart(2, '0') }} </button> </div> </ScrollArea> <ScrollArea ref="minCol" class="h-56"> <div class="flex w-14 flex-col p-1"> <button v-for="m in minutesList" :key="m" type="button" :data-active="parts?.minute === m" :disabled="isMinuteDisabled(m)" class="hover:bg-accent focus-visible:ring-ring rounded px-2 py-1 text-center text-sm tabular-nums transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:opacity-30 disabled:hover:bg-transparent" :class="parts?.minute === m ? 'bg-primary text-primary-foreground hover:bg-primary' : ''" @click="pickMinute(m)" > {{ String(m).padStart(2, '0') }} </button> </div> </ScrollArea> <ScrollArea v-if="showSeconds" ref="secCol" class="h-56"> <div class="flex w-14 flex-col p-1"> <button v-for="s in secondsList" :key="s" type="button" :data-active="parts?.second === s" :disabled="isSecondDisabled(s)" class="hover:bg-accent focus-visible:ring-ring rounded px-2 py-1 text-center text-sm tabular-nums transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:opacity-30 disabled:hover:bg-transparent" :class="parts?.second === s ? 'bg-primary text-primary-foreground hover:bg-primary' : ''" @click="pickSecond(s)" > {{ String(s).padStart(2, '0') }} </button> </div> </ScrollArea> <div v-if="effective12Hour" class="flex w-12 flex-col p-1"> <button v-for="p in ['AM', 'PM']" :key="p" type="button" class="hover:bg-accent focus-visible:ring-ring rounded px-2 py-1 text-center text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none" :class="period === p ? 'bg-primary text-primary-foreground hover:bg-primary' : ''" @click="pickPeriod(p as 'AM' | 'PM')" > {{ p }} </button> </div> </div> </template> -
app/components/ui/time-picker/TimePicker.vue 7.2 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed, ref, watch } from 'vue' import { Clock, X } from 'lucide-vue-next' import { Button } from '@/components/ui/button' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import TimeColumns, { type TimeFormat } from './TimeColumns.vue' import { cn } from '@/lib/utils' export interface TimePreset { label: string value: string } export type TimePickerSize = 'small' | 'middle' | 'large' export type TimePickerStatus = 'error' | 'warning' interface Props { /** Controlled value as 24h HH:mm or HH:mm:ss. Empty string = no selection. */ modelValue?: string placeholder?: string disabled?: boolean readOnly?: boolean clearable?: boolean allowClear?: boolean minuteStep?: number hourStep?: number secondStep?: number /** Backward-compat: when true, render 24-hour selector. */ use24Hour?: boolean /** When true, show AM/PM selector. */ use12Hours?: boolean minTime?: string maxTime?: string format?: TimeFormat disabledHours?: () => number[] disabledMinutes?: (selectedHour: number) => number[] disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[] hideDisabledOptions?: boolean presets?: TimePreset[] size?: TimePickerSize status?: TimePickerStatus triggerClass?: HTMLAttributes['class'] class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { modelValue: '', placeholder: 'Pick a time', disabled: false, readOnly: false, clearable: true, allowClear: undefined, minuteStep: 5, hourStep: 1, secondStep: 1, use24Hour: false, use12Hours: false, format: 'HH:mm', hideDisabledOptions: false, presets: () => [], size: 'middle', }) const emits = defineEmits<{ 'update:modelValue': [value: string] }>() const open = ref(false) const effectiveAllowClear = computed(() => { if (props.allowClear !== undefined) return props.allowClear return props.clearable }) function parse(v: string) { if (!v) return null const m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec(v.trim()) if (!m) return null return { hour24: Number(m[1]), minute: Number(m[2]), second: m[3] ? Number(m[3]) : 0 } } const parsed = computed(() => parse(props.modelValue)) const effective12Hour = computed(() => { if (props.format === 'hh:mm A') return true if (props.use12Hours) return true return !props.use24Hour }) function formatDisplay(h: number, m: number, s: number) { if (props.format === 'hh:mm A') { const h12 = h % 12 === 0 ? 12 : h % 12 const period = h >= 12 ? 'PM' : 'AM' return `${String(h12).padStart(2, '0')}:${String(m).padStart(2, '0')} ${period}` } if (props.format === 'HH:mm:ss') { return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` } return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}` } const display = computed(() => { if (!parsed.value) return '' const { hour24, minute, second } = parsed.value return formatDisplay(hour24, minute, second) }) function emitTime(v: string) { emits('update:modelValue', v) } function pickNow() { if (props.readOnly) return const d = new Date() const sStep = Math.max(1, props.secondStep) const rawS = d.getSeconds() const snappedS = Math.round(rawS / sStep) * sStep const secCarry = Math.floor(snappedS / 60) const s = snappedS % 60 const mStep = Math.max(1, props.minuteStep) const rawM = d.getMinutes() + secCarry const snappedM = Math.round(rawM / mStep) * mStep const minCarry = Math.floor(snappedM / 60) const min = snappedM % 60 const h = d.getHours() + minCarry const hourStr = String(h).padStart(2, '0') const minStr = String(min).padStart(2, '0') const secStr = String(s).padStart(2, '0') emitTime(props.format === 'HH:mm:ss' ? `${hourStr}:${minStr}:${secStr}` : `${hourStr}:${minStr}`) } function applyPreset(preset: TimePreset) { if (props.readOnly) return emitTime(preset.value) open.value = false } function clear(event: Event) { event.stopPropagation() if (props.disabled || props.readOnly) return emitTime('') } const sizeClasses: Record<TimePickerSize, string> = { small: 'h-8 text-xs px-2.5 py-1', middle: 'h-9 text-sm px-3 py-1.5', large: 'h-11 text-base px-4 py-2', } const statusClasses: Record<string, string> = { error: 'border-destructive focus-visible:ring-destructive', warning: 'border-warning focus-visible:ring-warning', } const triggerClasses = computed(() => cn( 'min-w-[160px] justify-start gap-2 text-left font-normal', !parsed.value && 'text-muted-foreground', sizeClasses[props.size], props.status && statusClasses[props.status], props.triggerClass, props.class, ), ) // Pulse the columns when reopen happens (passes via prop reactivity). const columnsVisible = ref(false) watch(open, (v) => { columnsVisible.value = v }) </script> <template> <Popover v-model:open="open"> <PopoverTrigger as-child> <Button type="button" variant="outline" :disabled="disabled" :class="triggerClasses" data-uipkge data-slot="time-picker" > <Clock class="size-4 shrink-0" /> <span class="flex-1 truncate">{{ display || placeholder }}</span> <button v-if="effectiveAllowClear && parsed && !disabled && !readOnly" type="button" class="text-muted-foreground hover:text-foreground focus-visible:ring-ring -mr-1 inline-flex size-5 items-center justify-center rounded transition-colors focus-visible:ring-2 focus-visible:outline-none" aria-label="Clear time" @click.stop="clear" > <X class="size-3.5" aria-hidden="true" /> </button> </Button> </PopoverTrigger> <PopoverContent class="w-auto p-0" align="start"> <div v-if="presets.length" class="flex flex-col gap-0.5 border-b p-2"> <button v-for="p in presets" :key="p.label" type="button" class="hover:bg-accent focus-visible:ring-ring rounded-md px-2 py-1.5 text-left text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none" @click="applyPreset(p)" > {{ p.label }} </button> </div> <div class="flex items-center justify-between border-b px-3 py-2"> <span class="text-muted-foreground text-xs tracking-widest uppercase">Time</span> <button type="button" class="text-muted-foreground hover:text-foreground focus-visible:ring-ring text-xs focus-visible:ring-2 focus-visible:outline-none" @click="pickNow" > Now </button> </div> <TimeColumns :model-value="modelValue" :use24-hour="use24Hour" :use12-hours="use12Hours" :minute-step="minuteStep" :hour-step="hourStep" :second-step="secondStep" :min-time="minTime" :max-time="maxTime" :format="format" :disabled-hours="disabledHours" :disabled-minutes="disabledMinutes" :disabled-seconds="disabledSeconds" :hide-disabled-options="hideDisabledOptions" :visible="columnsVisible" @update:model-value="emitTime" /> </PopoverContent> </Popover> </template> -
app/components/ui/time-picker/TimeRangePicker.vue 9.2 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed, ref, watch } from 'vue' import { Clock, X } from 'lucide-vue-next' import { Button } from '@/components/ui/button' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import TimeColumns, { type TimeFormat } from './TimeColumns.vue' import { cn } from '@/lib/utils' export interface TimeRangePreset { label: string value: [string, string] } export type TimeRangePickerSize = 'small' | 'middle' | 'large' export type TimeRangePickerStatus = 'error' | 'warning' interface Props { /** Controlled value as [startTime, endTime] in 24h format. */ modelValue?: [string, string] | null placeholder?: string disabled?: boolean readOnly?: boolean clearable?: boolean allowClear?: boolean minuteStep?: number hourStep?: number secondStep?: number use24Hour?: boolean use12Hours?: boolean minTime?: string maxTime?: string format?: TimeFormat disabledHours?: () => number[] disabledMinutes?: (selectedHour: number) => number[] disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[] hideDisabledOptions?: boolean presets?: TimeRangePreset[] size?: TimeRangePickerSize status?: TimeRangePickerStatus triggerClass?: HTMLAttributes['class'] class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { modelValue: null, placeholder: 'Pick a time range', disabled: false, readOnly: false, clearable: true, allowClear: undefined, minuteStep: 5, hourStep: 1, secondStep: 1, use24Hour: false, use12Hours: false, format: 'HH:mm', hideDisabledOptions: false, presets: () => [], size: 'middle', }) const emits = defineEmits<{ 'update:modelValue': [value: [string, string] | null] }>() const open = ref(false) const effectiveAllowClear = computed(() => { if (props.allowClear !== undefined) return props.allowClear return props.clearable }) function parse(v: string) { if (!v) return null const m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec(v.trim()) if (!m) return null return { hour24: Number(m[1]), minute: Number(m[2]), second: m[3] ? Number(m[3]) : 0 } } const startValue = computed(() => props.modelValue?.[0] ?? '') const endValue = computed(() => props.modelValue?.[1] ?? '') const startParsed = computed(() => parse(startValue.value)) const endParsed = computed(() => parse(endValue.value)) const effective12Hour = computed(() => { if (props.format === 'hh:mm A') return true if (props.use12Hours) return true return !props.use24Hour }) function formatDisplay(h: number, m: number, s: number) { if (props.format === 'hh:mm A') { const h12 = h % 12 === 0 ? 12 : h % 12 const period = h >= 12 ? 'PM' : 'AM' return `${String(h12).padStart(2, '0')}:${String(m).padStart(2, '0')} ${period}` } if (props.format === 'HH:mm:ss') { return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` } return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}` } const display = computed(() => { const hasStart = startParsed.value !== null const hasEnd = endParsed.value !== null if (!hasStart && !hasEnd) return '' const startStr = hasStart ? formatDisplay(startParsed.value!.hour24, startParsed.value!.minute, startParsed.value!.second) : '' const endStr = hasEnd ? formatDisplay(endParsed.value!.hour24, endParsed.value!.minute, endParsed.value!.second) : '' if (hasStart && hasEnd) return `${startStr} ~ ${endStr}` return startStr || endStr }) function emitRange(start: string, end: string) { emits('update:modelValue', [start, end]) } function emitStart(v: string) { emitRange(v, endValue.value || v) } function emitEnd(v: string) { emitRange(startValue.value || v, v) } function pickNow(which: 'start' | 'end') { if (props.readOnly) return const d = new Date() const sStep = Math.max(1, props.secondStep) const rawS = d.getSeconds() const snappedS = Math.round(rawS / sStep) * sStep const secCarry = Math.floor(snappedS / 60) const s = snappedS % 60 const mStep = Math.max(1, props.minuteStep) const rawM = d.getMinutes() + secCarry const snappedM = Math.round(rawM / mStep) * mStep const minCarry = Math.floor(snappedM / 60) const min = snappedM % 60 const h = d.getHours() + minCarry const hourStr = String(h).padStart(2, '0') const minStr = String(min).padStart(2, '0') const secStr = String(s).padStart(2, '0') const v = props.format === 'HH:mm:ss' ? `${hourStr}:${minStr}:${secStr}` : `${hourStr}:${minStr}` if (which === 'start') emitStart(v) else emitEnd(v) } function applyPreset(preset: TimeRangePreset) { if (props.readOnly) return emits('update:modelValue', preset.value) open.value = false } function clear(event: Event) { event.stopPropagation() if (props.disabled || props.readOnly) return emits('update:modelValue', null) } const sizeClasses: Record<TimeRangePickerSize, string> = { small: 'h-8 text-xs px-2.5 py-1', middle: 'h-9 text-sm px-3 py-1.5', large: 'h-11 text-base px-4 py-2', } const statusClasses: Record<string, string> = { error: 'border-destructive focus-visible:ring-destructive', warning: 'border-warning focus-visible:ring-warning', } const triggerClasses = computed(() => cn( 'min-w-[200px] justify-start gap-2 text-left font-normal', !display.value && 'text-muted-foreground', sizeClasses[props.size], props.status && statusClasses[props.status], props.triggerClass, props.class, ), ) const columnsVisible = ref(false) watch(open, (v) => { columnsVisible.value = v }) </script> <template> <Popover v-model:open="open"> <PopoverTrigger as-child> <Button type="button" variant="outline" :disabled="disabled" :class="triggerClasses" data-uipkge data-slot="time-range-picker" > <Clock class="size-4 shrink-0" /> <span class="flex-1 truncate">{{ display || placeholder }}</span> <button v-if="effectiveAllowClear && display && !disabled && !readOnly" type="button" class="text-muted-foreground hover:text-foreground focus-visible:ring-ring -mr-1 inline-flex size-5 items-center justify-center rounded transition-colors focus-visible:ring-2 focus-visible:outline-none" aria-label="Clear time range" @click.stop="clear" > <X class="size-3.5" aria-hidden="true" /> </button> </Button> </PopoverTrigger> <PopoverContent class="w-auto p-0" align="start"> <div v-if="presets.length" class="flex flex-col gap-0.5 border-b p-2"> <button v-for="p in presets" :key="p.label" type="button" class="hover:bg-accent focus-visible:ring-ring rounded-md px-2 py-1.5 text-left text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none" @click="applyPreset(p)" > {{ p.label }} </button> </div> <div class="flex items-center justify-between border-b px-3 py-2"> <span class="text-muted-foreground text-xs tracking-widest uppercase">Time Range</span> <div class="flex gap-2"> <button type="button" class="text-muted-foreground hover:text-foreground focus-visible:ring-ring text-xs focus-visible:ring-2 focus-visible:outline-none" @click="pickNow('start')" > Now (Start) </button> <button type="button" class="text-muted-foreground hover:text-foreground text-xs" @click="pickNow('end')"> Now (End) </button> </div> </div> <div class="flex"> <div class="flex flex-col"> <div class="text-muted-foreground border-b px-3 py-1.5 text-center text-xs font-medium">Start</div> <TimeColumns :model-value="startValue" :use24-hour="use24Hour" :use12-hours="use12Hours" :minute-step="minuteStep" :hour-step="hourStep" :second-step="secondStep" :min-time="minTime" :max-time="maxTime" :format="format" :disabled-hours="disabledHours" :disabled-minutes="disabledMinutes" :disabled-seconds="disabledSeconds" :hide-disabled-options="hideDisabledOptions" :visible="columnsVisible" @update:model-value="emitStart" /> </div> <div class="bg-border w-px" /> <div class="flex flex-col"> <div class="text-muted-foreground border-b px-3 py-1.5 text-center text-xs font-medium">End</div> <TimeColumns :model-value="endValue" :use24-hour="use24Hour" :use12-hours="use12Hours" :minute-step="minuteStep" :hour-step="hourStep" :second-step="secondStep" :min-time="minTime" :max-time="maxTime" :format="format" :disabled-hours="disabledHours" :disabled-minutes="disabledMinutes" :disabled-seconds="disabledSeconds" :hide-disabled-options="hideDisabledOptions" :visible="columnsVisible" @update:model-value="emitEnd" /> </div> </div> </PopoverContent> </Popover> </template> -
app/components/ui/time-picker/index.ts 0.2 kB
export { default as TimePicker } from './TimePicker.vue' export { default as TimeRangePicker } from './TimeRangePicker.vue' export { default as TimeColumns } from './TimeColumns.vue'
Raw manifest: https://uipkge.dev/r/vue/time-picker.json