@meistrari/tela-build 1.60.0 → 1.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ import { cva } from 'class-variance-authority'
2
+ import type { VariantProps } from 'class-variance-authority'
3
+
4
+ export const buttonVariants = cva('', {
5
+ variants: {
6
+ variant: {
7
+ 'primary': 'bg-btn-primary text-inverse hover:bg-btn-primary-hover active:bg-btn-primary-active focus-visible:ring-0.5px focus-visible:ring-cyan-600',
8
+ 'secondary': 'bg-btn-secondary text-contrast border border-[0.5px] [box-shadow:0_1px_6px_0_rgba(103,127,148,0.05)] hover:bg-btn-secondary-hover hover:border-strong active:bg-btn-secondary-active active:border-neutral-400/60 focus-visible:ring-0.5px focus-visible:ring-cyan-600',
9
+ 'ghost': 'bg-btn-ghost text-contrast hover:bg-btn-ghost-hover focus-visible:ring-0.5px focus-visible:ring-cyan-600',
10
+ 'disabled': 'bg-btn-disabled text-tertiary !cursor-not-allowed',
11
+ 'loading': 'bg-btn-loading text-neutral-600',
12
+ 'danger': 'bg-btn-danger text-red-500 hover:bg-btn-danger-hover focus:outline-red-600',
13
+ 'ghost-danger': 'bg-transparent border-[0.5px] border-transparent text-red-600 hover:bg-btn-danger-hover hover:text-red-700 focus:outline-red-600 focus:bg-btn-danger-hover',
14
+ 'dark': 'bg-btn-dark text-white border-[0.5px] border-white/16 hover:bg-btn-dark-hover active:bg-btn-dark-active focus-visible:ring-0.5px focus-visible:ring-cyan-600',
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: 'primary',
19
+ },
20
+ })
21
+
22
+ export type ButtonVariants = VariantProps<typeof buttonVariants>
@@ -1,8 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import { NuxtLink } from '#components'
3
+ import { buttonVariants } from './button-variants'
3
4
 
4
5
  export type ButtonSize = 'sm' | 'md' | 'lg'
5
- export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'success' | 'danger' | 'ghost-danger' | 'dark'
6
+ export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'ghost-danger' | 'dark'
6
7
 
7
8
  export type ButtonProps = {
8
9
  size?: ButtonSize
@@ -18,12 +19,15 @@ export type ButtonProps = {
18
19
  const props = defineProps<ButtonProps>()
19
20
 
20
21
  const resolvedSize = computed(() => props.size || 'md')
21
- const resolvedVariant = computed(() =>
22
- (props.disabled && 'disabled')
23
- || (props.loading && 'loading')
24
- || props.variant
25
- || 'primary',
26
- )
22
+ const resolvedVariant = computed<ButtonVariant | 'disabled' | 'loading'>(() => {
23
+ if (props.disabled)
24
+ return 'disabled'
25
+
26
+ if (props.loading)
27
+ return 'loading'
28
+
29
+ return props.variant || 'primary'
30
+ })
27
31
 
28
32
  const slots = useSlots()
29
33
 
@@ -41,48 +45,6 @@ const paddingStyle = computed(() => [
41
45
  hasTrailingIcon.value ? 'pr-[calc(var(--padding-x)_-_2px)]' : 'pr-[var(--padding-x)]',
42
46
  ].join(' '))
43
47
 
44
- const variantStyle = computed(() => (({
45
- 'primary': fold`
46
- bg-btn-primary text-inverse
47
- hover:bg-btn-primary-hover
48
- active:bg-btn-primary-active
49
- focus-visible:ring-0.5px focus-visible:ring-cyan-600
50
- `,
51
- 'secondary': fold`
52
- bg-btn-secondary text-contrast border border-[0.5px]
53
- [box-shadow:0_1px_6px_0_rgba(103,127,148,0.05)]
54
- hover:bg-btn-secondary-hover hover:border-strong
55
- active:bg-btn-secondary-active active:border-neutral-400/60
56
- focus-visible:ring-0.5px focus-visible:ring-cyan-600
57
- `,
58
- 'ghost': fold`
59
- bg-btn-ghost text-contrast
60
- hover:bg-btn-ghost-hover
61
- focus-visible:ring-0.5px focus-visible:ring-cyan-600
62
- `,
63
- 'disabled': fold`
64
- bg-btn-disabled text-tertiary !cursor-not-allowed
65
- `,
66
- 'loading': fold`
67
- bg-btn-loading text-neutral-600
68
- `,
69
- 'danger': fold`
70
- bg-btn-danger text-red-500
71
- hover:bg-btn-danger-hover
72
- focus:outline-red-600
73
- `,
74
- 'ghost-danger': fold`
75
- bg-transparent border-[0.5px] border-transparent text-red-600
76
- hover:bg-red-50 hover:text-red-700
77
- focus:outline-red-600
78
- `,
79
- 'dark': fold`
80
- bg-btn-dark text-white border-[0.5px] border-white/16
81
- hover:bg-btn-dark-hover active:bg-btn-dark-active
82
- focus-visible:ring-0.5px focus-visible:ring-cyan-600
83
- `,
84
- }) as Record<ButtonVariant | 'disabled' | 'loading', string>)[resolvedVariant.value] ?? '')
85
-
86
48
  const iconColorStyle = computed(() => (({
87
49
  'primary': 'icon-inverse',
88
50
  'secondary': 'icon-secondary',
@@ -97,7 +59,7 @@ const iconColorStyle = computed(() => (({
97
59
  const resolvedStyle = computed(() => [
98
60
  sizeStyle.value,
99
61
  paddingStyle.value,
100
- variantStyle.value,
62
+ buttonVariants({ variant: resolvedVariant.value }),
101
63
  ].join(' '))
102
64
 
103
65
  const tag = computed(() => props.to ? NuxtLink : 'button')
@@ -27,11 +27,9 @@ const emit = defineEmits<{
27
27
  const state = ref<ButtonState>('normal')
28
28
  const timeoutId = ref<ReturnType<typeof setTimeout>>()
29
29
 
30
- const buttonProps = computed(() => ({
30
+ const buttonProps = computed<TelaButtonProps>(() => ({
31
31
  size: props.size,
32
- variant: (state.value === 'success' && 'success')
33
- || (state.value === 'error' && 'danger')
34
- || props.variant,
32
+ variant: state.value === 'error' ? 'danger' : props.variant,
35
33
  icon: props.icon,
36
34
  loading: state.value === 'loading',
37
35
  leading: props.leading,
@@ -25,12 +25,14 @@ import {
25
25
  import type { DateRange } from 'radix-vue'
26
26
  import { computed } from 'vue'
27
27
 
28
- const props = defineProps<{
28
+ const props = withDefaults(defineProps<{
29
29
  label?: string
30
30
  disableClear?: boolean
31
31
  open?: boolean
32
32
  hasClear?: boolean
33
- }>()
33
+ }>(), {
34
+ open: undefined,
35
+ })
34
36
 
35
37
  const emit = defineEmits<{
36
38
  (e: 'update:model-value', value: DateRange): void
@@ -0,0 +1,41 @@
1
+ <script lang="ts" setup>
2
+ import type { RangeCalendarCellTriggerProps } from 'reka-ui'
3
+ import type { HTMLAttributes } from 'vue'
4
+ import { reactiveOmit } from '@vueuse/core'
5
+ import { RangeCalendarCellTrigger, useForwardProps } from 'reka-ui'
6
+
7
+ import { buttonVariants } from '../button/button-variants'
8
+
9
+ const props = withDefaults(defineProps<RangeCalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>(), {
10
+ as: 'button',
11
+ })
12
+
13
+ const delegatedProps = reactiveOmit(props, 'class')
14
+
15
+ const forwardedProps = useForwardProps(delegatedProps)
16
+ </script>
17
+
18
+ <template>
19
+ <RangeCalendarCellTrigger
20
+ data-slot="range-calendar-cell-trigger"
21
+ :class="cn(
22
+ buttonVariants({ variant: 'ghost' }),
23
+ 'size-[32px] rounded-[8px] p-0 body-14-medium data-[selected]:opacity-100',
24
+ '[&[data-today]:not([data-selected])]:bg-neutral-900 [&[data-today]:not([data-selected])]:text-white',
25
+ // Selection Start
26
+ 'data-[selection-start]:bg-neutral-900 data-[selection-start]:text-white [&[data-selection-start]:hover]:bg-neutral-900 data-[selection-start]:hover:text-white data-[selection-start]:focus:bg-neutral-900 data-[selection-start]:focus:text-white',
27
+ // Selection End
28
+ 'data-[selection-end]:bg-neutral-800 data-[selection-end]:text-white [&[data-selection-end]:hover]:bg-neutral-900 data-[selection-end]:hover:text-white data-[selection-end]:focus:bg-neutral-900 data-[selection-end]:focus:text-white',
29
+ // Outside months
30
+ 'data-[outside-view]:invisible',
31
+ // Disabled
32
+ 'data-[disabled]:text-tertiary data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed data-[disabled]:hover:bg-transparent',
33
+ // Unavailable
34
+ 'data-[unavailable]:text-subtle data-[unavailable]:line-through data-[unavailable]:hover:bg-transparent',
35
+ props.class,
36
+ )"
37
+ v-bind="forwardedProps"
38
+ >
39
+ <slot />
40
+ </RangeCalendarCellTrigger>
41
+ </template>
@@ -0,0 +1,26 @@
1
+ <script lang="ts" setup>
2
+ import type { RangeCalendarCellProps } from 'reka-ui'
3
+ import type { HTMLAttributes } from 'vue'
4
+ import { reactiveOmit } from '@vueuse/core'
5
+ import { RangeCalendarCell, useForwardProps } from 'reka-ui'
6
+
7
+ const props = defineProps<RangeCalendarCellProps & { class?: HTMLAttributes['class'] }>()
8
+
9
+ const delegatedProps = reactiveOmit(props, 'class')
10
+
11
+ const forwardedProps = useForwardProps(delegatedProps)
12
+ </script>
13
+
14
+ <template>
15
+ <RangeCalendarCell
16
+ data-slot="range-calendar-cell"
17
+ :class="cn(
18
+ 'relative p-0 text-center body-12-semibold focus-within:relative rounded-[8px] focus-within:z-20',
19
+ '[&:has([data-selected]):not(:has([data-outside-view]))]:bg-muted',
20
+ props.class,
21
+ )"
22
+ v-bind="forwardedProps"
23
+ >
24
+ <slot />
25
+ </RangeCalendarCell>
26
+ </template>
@@ -0,0 +1,20 @@
1
+ <script lang="ts" setup>
2
+ import type { RangeCalendarGridBodyProps } from 'reka-ui'
3
+ import type { HTMLAttributes } from 'vue'
4
+ import { reactiveOmit } from '@vueuse/core'
5
+ import { RangeCalendarGridBody } from 'reka-ui'
6
+
7
+ const props = defineProps<RangeCalendarGridBodyProps & { class?: HTMLAttributes['class'] }>()
8
+
9
+ const delegatedProps = reactiveOmit(props, 'class')
10
+ </script>
11
+
12
+ <template>
13
+ <RangeCalendarGridBody
14
+ data-slot="range-calendar-grid-body"
15
+ :class="cn(props.class)"
16
+ v-bind="delegatedProps"
17
+ >
18
+ <slot />
19
+ </RangeCalendarGridBody>
20
+ </template>
@@ -0,0 +1,20 @@
1
+ <script lang="ts" setup>
2
+ import type { RangeCalendarGridHeadProps } from 'reka-ui'
3
+ import type { HTMLAttributes } from 'vue'
4
+ import { reactiveOmit } from '@vueuse/core'
5
+ import { RangeCalendarGridHead } from 'reka-ui'
6
+
7
+ const props = defineProps<RangeCalendarGridHeadProps & { class?: HTMLAttributes['class'] }>()
8
+
9
+ const delegatedProps = reactiveOmit(props, 'class')
10
+ </script>
11
+
12
+ <template>
13
+ <RangeCalendarGridHead
14
+ data-slot="range-calendar-grid-head"
15
+ :class="cn(props.class)"
16
+ v-bind="delegatedProps"
17
+ >
18
+ <slot />
19
+ </RangeCalendarGridHead>
20
+ </template>
@@ -0,0 +1,20 @@
1
+ <script lang="ts" setup>
2
+ import type { RangeCalendarGridRowProps } from 'reka-ui'
3
+ import type { HTMLAttributes } from 'vue'
4
+ import { reactiveOmit } from '@vueuse/core'
5
+ import { RangeCalendarGridRow } from 'reka-ui'
6
+
7
+ const props = defineProps<RangeCalendarGridRowProps & { class?: HTMLAttributes['class'] }>()
8
+
9
+ const delegatedProps = reactiveOmit(props, 'class')
10
+ </script>
11
+
12
+ <template>
13
+ <RangeCalendarGridRow
14
+ data-slot="range-calendar-grid-row"
15
+ :class="cn('flex gap-[2px] mt-[2px]', props.class)"
16
+ v-bind="delegatedProps"
17
+ >
18
+ <slot />
19
+ </RangeCalendarGridRow>
20
+ </template>
@@ -0,0 +1,22 @@
1
+ <script lang="ts" setup>
2
+ import type { RangeCalendarGridProps } from 'reka-ui'
3
+ import type { HTMLAttributes } from 'vue'
4
+ import { reactiveOmit } from '@vueuse/core'
5
+ import { RangeCalendarGrid, useForwardProps } from 'reka-ui'
6
+
7
+ const props = defineProps<RangeCalendarGridProps & { class?: HTMLAttributes['class'] }>()
8
+
9
+ const delegatedProps = reactiveOmit(props, 'class')
10
+
11
+ const forwardedProps = useForwardProps(delegatedProps)
12
+ </script>
13
+
14
+ <template>
15
+ <RangeCalendarGrid
16
+ data-slot="range-calendar-grid"
17
+ :class="cn('w-full border-collapse select-none', props.class)"
18
+ v-bind="forwardedProps"
19
+ >
20
+ <slot />
21
+ </RangeCalendarGrid>
22
+ </template>
@@ -0,0 +1,20 @@
1
+ <script lang="ts" setup>
2
+ import type { RangeCalendarHeadCellProps } from 'reka-ui'
3
+ import type { HTMLAttributes } from 'vue'
4
+ import { reactiveOmit } from '@vueuse/core'
5
+ import { RangeCalendarHeadCell } from 'reka-ui'
6
+
7
+ const props = defineProps<RangeCalendarHeadCellProps & { class?: HTMLAttributes['class'] }>()
8
+
9
+ const delegatedProps = reactiveOmit(props, 'class')
10
+ </script>
11
+
12
+ <template>
13
+ <RangeCalendarHeadCell
14
+ data-slot="range-calendar-head-cell"
15
+ :class="cn('w-[32px] rounded-[6px] body-12-semibold text-tertiary', props.class)"
16
+ v-bind="delegatedProps"
17
+ >
18
+ <slot />
19
+ </RangeCalendarHeadCell>
20
+ </template>
@@ -0,0 +1,20 @@
1
+ <script lang="ts" setup>
2
+ import type { RangeCalendarHeaderProps } from 'reka-ui'
3
+ import type { HTMLAttributes } from 'vue'
4
+ import { reactiveOmit } from '@vueuse/core'
5
+ import { RangeCalendarHeader } from 'reka-ui'
6
+
7
+ const props = defineProps<RangeCalendarHeaderProps & { class?: HTMLAttributes['class'] }>()
8
+
9
+ const delegatedProps = reactiveOmit(props, 'class')
10
+ </script>
11
+
12
+ <template>
13
+ <RangeCalendarHeader
14
+ data-slot="range-calendar-header"
15
+ :class="cn('flex items-center justify-between', props.class)"
16
+ v-bind="delegatedProps"
17
+ >
18
+ <slot />
19
+ </RangeCalendarHeader>
20
+ </template>
@@ -0,0 +1,22 @@
1
+ <script lang="ts" setup>
2
+ import type { RangeCalendarHeadingProps } from 'reka-ui'
3
+ import type { HTMLAttributes } from 'vue'
4
+ import { reactiveOmit } from '@vueuse/core'
5
+ import { RangeCalendarHeading, useForwardProps } from 'reka-ui'
6
+
7
+ const props = defineProps<RangeCalendarHeadingProps & { class?: HTMLAttributes['class'] }>()
8
+
9
+ const delegatedProps = reactiveOmit(props, 'class')
10
+
11
+ const forwardedProps = useForwardProps(delegatedProps)
12
+ </script>
13
+
14
+ <template>
15
+ <RangeCalendarHeading
16
+ data-slot="range-calendar-heading"
17
+ :class="cn('body-14-semibold text-primary', props.class)"
18
+ v-bind="forwardedProps"
19
+ >
20
+ <slot />
21
+ </RangeCalendarHeading>
22
+ </template>
@@ -0,0 +1,30 @@
1
+ <script lang="ts" setup>
2
+ import type { RangeCalendarNextProps } from 'reka-ui'
3
+ import type { HTMLAttributes } from 'vue'
4
+ import { reactiveOmit } from '@vueuse/core'
5
+ import { RangeCalendarNext, useForwardProps } from 'reka-ui'
6
+ import { buttonVariants } from '../button/button-variants'
7
+
8
+ const props = defineProps<RangeCalendarNextProps & { class?: HTMLAttributes['class'] }>()
9
+
10
+ const delegatedProps = reactiveOmit(props, 'class')
11
+
12
+ const forwardedProps = useForwardProps(delegatedProps)
13
+ </script>
14
+
15
+ <template>
16
+ <RangeCalendarNext
17
+ data-slot="range-calendar-next-button"
18
+ :class="cn(
19
+ buttonVariants({ variant: 'secondary' }),
20
+ 'flex shrink-0 items-center justify-center cursor-pointer rounded-9px size-[32px] transition-all active:scale-98',
21
+ 'data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed data-[disabled]:hover:bg-transparent',
22
+ props.class,
23
+ )"
24
+ v-bind="forwardedProps"
25
+ >
26
+ <slot>
27
+ <TelaIcon name="i-ph-caret-right" color="icon-secondary" size="18px" />
28
+ </slot>
29
+ </RangeCalendarNext>
30
+ </template>
@@ -0,0 +1,30 @@
1
+ <script lang="ts" setup>
2
+ import type { RangeCalendarPrevProps } from 'reka-ui'
3
+ import type { HTMLAttributes } from 'vue'
4
+ import { reactiveOmit } from '@vueuse/core'
5
+ import { RangeCalendarPrev, useForwardProps } from 'reka-ui'
6
+ import { buttonVariants } from '../button/button-variants'
7
+
8
+ const props = defineProps<RangeCalendarPrevProps & { class?: HTMLAttributes['class'] }>()
9
+
10
+ const delegatedProps = reactiveOmit(props, 'class')
11
+
12
+ const forwardedProps = useForwardProps(delegatedProps)
13
+ </script>
14
+
15
+ <template>
16
+ <RangeCalendarPrev
17
+ data-slot="range-calendar-prev-button"
18
+ :class="cn(
19
+ buttonVariants({ variant: 'secondary' }),
20
+ 'flex shrink-0 items-center justify-center cursor-pointer rounded-9px size-[32px] transition-all active:scale-98',
21
+ 'data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed data-[disabled]:hover:bg-transparent',
22
+ props.class,
23
+ )"
24
+ v-bind="forwardedProps"
25
+ >
26
+ <slot>
27
+ <TelaIcon name="i-ph-caret-left" color="icon-secondary" size="18px" />
28
+ </slot>
29
+ </RangeCalendarPrev>
30
+ </template>
@@ -0,0 +1,224 @@
1
+ <script lang="ts" setup>
2
+ import { Time } from '@internationalized/date'
3
+ import type { HTMLAttributes } from 'vue'
4
+ import { TimeFieldInput, TimeFieldRoot } from 'reka-ui'
5
+
6
+ const props = defineProps<{
7
+ startTime?: Time
8
+ endTime?: Time
9
+ disabled?: boolean
10
+ locale?: string
11
+ class?: HTMLAttributes['class']
12
+ }>()
13
+
14
+ const emits = defineEmits<{
15
+ 'update:startTime': [value: Time]
16
+ 'update:endTime': [value: Time]
17
+ }>()
18
+
19
+ function handleUpdate(part: 'startTime' | 'endTime', value?: { hour: number, minute: number }) {
20
+ if (!value)
21
+ return
22
+
23
+ const time = new Time(value.hour, value.minute)
24
+
25
+ if (part === 'startTime')
26
+ emits('update:startTime', time)
27
+ else
28
+ emits('update:endTime', time)
29
+ }
30
+
31
+ const startFieldRef = ref<InstanceType<typeof TimeFieldRoot>>()
32
+ const endFieldRef = ref<InstanceType<typeof TimeFieldRoot>>()
33
+
34
+ function editableSegments(field?: HTMLElement) {
35
+ if (!field)
36
+ return []
37
+
38
+ return Array.from(field.querySelectorAll<HTMLElement>('[data-reka-time-field-segment]:not([data-reka-time-field-segment="literal"])'))
39
+ }
40
+
41
+ const minutePrev = ref<number | null>(null)
42
+ const minuteLeftFocus = ref(true)
43
+ const minuteLastKeyZero = ref(false)
44
+
45
+ function focusEndField() {
46
+ nextTick(() => {
47
+ editableSegments(endFieldRef.value?.$el).at(0)?.focus()
48
+ })
49
+ }
50
+
51
+ function focusStartFieldLastSegment() {
52
+ nextTick(() => {
53
+ editableSegments(startFieldRef.value?.$el).at(-1)?.focus()
54
+ })
55
+ }
56
+
57
+ function isLastEditableSegment(target: HTMLElement, field: HTMLElement) {
58
+ const segments = editableSegments(field)
59
+ return segments.length > 0 && segments[segments.length - 1] === target
60
+ }
61
+
62
+ function isFirstEditableSegment(target: HTMLElement, field: HTMLElement) {
63
+ return editableSegments(field)[0] === target
64
+ }
65
+
66
+ function handleEndKeydown(event: KeyboardEvent) {
67
+ if (event.key !== 'ArrowLeft')
68
+ return
69
+
70
+ const target = event.target as HTMLElement
71
+
72
+ if (isFirstEditableSegment(target, event.currentTarget as HTMLElement))
73
+ focusStartFieldLastSegment()
74
+ }
75
+
76
+ function handleStartFocusout(event: FocusEvent) {
77
+ const target = event.target as HTMLElement
78
+ if (target.dataset?.rekaTimeFieldSegment === 'minute')
79
+ minuteLeftFocus.value = true
80
+ }
81
+
82
+ function handleStartKeydown(event: KeyboardEvent) {
83
+ const target = event.target as HTMLElement
84
+ const segment = target.dataset?.rekaTimeFieldSegment
85
+
86
+ if (!isLastEditableSegment(target, event.currentTarget as HTMLElement))
87
+ return
88
+
89
+ if (event.key === 'ArrowRight') {
90
+ focusEndField()
91
+ return
92
+ }
93
+
94
+ if (segment === 'dayPeriod') {
95
+ if (['a', 'p'].includes(event.key.toLowerCase()))
96
+ focusEndField()
97
+ return
98
+ }
99
+
100
+ if (segment !== 'minute')
101
+ return
102
+
103
+ if (event.key === 'Backspace') {
104
+ minuteLeftFocus.value = false
105
+ minutePrev.value = minutePrev.value !== null && minutePrev.value >= 10
106
+ ? Math.floor(minutePrev.value / 10)
107
+ : null
108
+ return
109
+ }
110
+
111
+ if (!/^\d$/.test(event.key))
112
+ return
113
+
114
+ const num = Number(event.key)
115
+
116
+ if (minuteLeftFocus.value) {
117
+ minuteLeftFocus.value = false
118
+ minutePrev.value = null
119
+ }
120
+
121
+ if (minutePrev.value === null) {
122
+ if (num === 0) {
123
+ minuteLastKeyZero.value = true
124
+ minutePrev.value = 0
125
+ return
126
+ }
127
+
128
+ const moveToNext = minuteLastKeyZero.value || num > 5
129
+ minuteLastKeyZero.value = false
130
+ minutePrev.value = num
131
+
132
+ if (moveToNext)
133
+ focusEndField()
134
+ return
135
+ }
136
+
137
+ const digits = minutePrev.value.toString().length
138
+ const total = Number.parseInt(`${minutePrev.value}${num}`)
139
+
140
+ if (digits === 2 || total > 59) {
141
+ minutePrev.value = num
142
+
143
+ if (num > 5)
144
+ focusEndField()
145
+ return
146
+ }
147
+
148
+ minutePrev.value = total
149
+ focusEndField()
150
+ }
151
+ </script>
152
+
153
+ <template>
154
+ <div
155
+ data-slot="range-calendar-time-field"
156
+ :class="cn(
157
+ 'grid grid-cols-[1fr_auto_1fr] h-32px select-none items-center rounded-[8px] border-0.5px border bg px-8px body-14-medium',
158
+ 'focus-within:border-strong focus-within:bg-subtle',
159
+ props.disabled && 'opacity-50 cursor-not-allowed',
160
+ props.class,
161
+ )"
162
+ >
163
+ <TimeFieldRoot
164
+ ref="startFieldRef"
165
+ v-slot="{ segments }"
166
+ :model-value="props.startTime"
167
+ :disabled="props.disabled"
168
+ :locale="props.locale"
169
+ granularity="minute"
170
+ class="flex items-center gap-1px justify-self-start"
171
+ @update:model-value="handleUpdate('startTime', $event)"
172
+ @keydown="handleStartKeydown"
173
+ @focusout="handleStartFocusout"
174
+ >
175
+ <template v-for="(segment, index) in segments" :key="`start-${segment.part}-${index}`">
176
+ <TimeFieldInput
177
+ v-if="segment.part === 'literal'"
178
+ :part="segment.part"
179
+ class="text-tertiary"
180
+ >
181
+ {{ segment.value }}
182
+ </TimeFieldInput>
183
+ <TimeFieldInput
184
+ v-else
185
+ :part="segment.part"
186
+ class="rounded-4px px-2px tabular-nums outline-none focus:bg-lowered data-[placeholder]:text-tertiary"
187
+ >
188
+ {{ segment.value }}
189
+ </TimeFieldInput>
190
+ </template>
191
+ </TimeFieldRoot>
192
+
193
+ <span class="text-primary pointer-events-none">–</span>
194
+
195
+ <TimeFieldRoot
196
+ ref="endFieldRef"
197
+ v-slot="{ segments }"
198
+ :model-value="props.endTime"
199
+ :disabled="props.disabled"
200
+ :locale="props.locale"
201
+ granularity="minute"
202
+ class="flex items-center gap-1px justify-self-end"
203
+ @update:model-value="handleUpdate('endTime', $event)"
204
+ @keydown="handleEndKeydown"
205
+ >
206
+ <template v-for="(segment, index) in segments" :key="`end-${segment.part}-${index}`">
207
+ <TimeFieldInput
208
+ v-if="segment.part === 'literal'"
209
+ :part="segment.part"
210
+ class="text-tertiary"
211
+ >
212
+ {{ segment.value }}
213
+ </TimeFieldInput>
214
+ <TimeFieldInput
215
+ v-else
216
+ :part="segment.part"
217
+ class="rounded-4px px-2px tabular-nums outline-none focus:bg-lowered data-[placeholder]:text-tertiary"
218
+ >
219
+ {{ segment.value }}
220
+ </TimeFieldInput>
221
+ </template>
222
+ </TimeFieldRoot>
223
+ </div>
224
+ </template>
@@ -0,0 +1,155 @@
1
+ # TelaRangeCalendar
2
+
3
+ A two-handle calendar for picking a start and end date, with an optional time row for narrowing the range down to the minute. Built on `reka-ui`'s `RangeCalendarRoot` primitives and `@internationalized/date`, so values are `DateValue` objects — not JavaScript `Date` — and stay calendar- and timezone-correct.
4
+
5
+ The header is not a static month label: it renders month and year as two `TelaSelectMenu` triggers, so jumping to a distant month takes one click instead of paging. The year list spans the placeholder year ±10.
6
+
7
+ ## Examples
8
+
9
+ ### Basic Usage
10
+
11
+ `modelValue` is a `DateRange` — `{ start, end }`, both `DateValue`. Clicking once sets `start`, clicking again sets `end`; the days in between render as the highlighted span.
12
+
13
+ ```vue
14
+ <script setup>
15
+ import { ref } from 'vue'
16
+
17
+ const range = ref()
18
+ </script>
19
+
20
+ <template>
21
+ <TelaRangeCalendar v-model="range" />
22
+ </template>
23
+ ```
24
+
25
+ ### Preselected Range
26
+
27
+ Pass an initial `DateRange` built from `@internationalized/date` helpers. `today(getLocalTimeZone())` returns a `CalendarDate`, and `.add()` returns a new one — these values are immutable.
28
+
29
+ ```vue
30
+ <script setup>
31
+ import { getLocalTimeZone, today } from '@internationalized/date'
32
+ import { ref } from 'vue'
33
+
34
+ const start = today(getLocalTimeZone())
35
+ const range = ref({ start, end: start.add({ days: 6 }) })
36
+ </script>
37
+
38
+ <template>
39
+ <TelaRangeCalendar v-model="range" />
40
+ </template>
41
+ ```
42
+
43
+ ### With Time
44
+
45
+ `with-time` appends a single time-range field below the grid — start and end time in one input, separated by a dash. The times are merged into the emitted range, so `start` and `end` come back as `CalendarDateTime` instead of `CalendarDate`. Editing a time re-emits the range immediately — no need to reselect the days.
46
+
47
+ Times default to `00:00` and `23:59`, so an untouched selection covers the full span of both days.
48
+
49
+ ```vue
50
+ <script setup>
51
+ import { ref } from 'vue'
52
+
53
+ const range = ref()
54
+ </script>
55
+
56
+ <template>
57
+ <div class="flex flex-col gap-12px">
58
+ <TelaRangeCalendar v-model="range" with-time />
59
+ <pre class="body-12-medium text-secondary">{{ range ?? 'No range selected' }}</pre>
60
+ </div>
61
+ </template>
62
+ ```
63
+
64
+ ### Multiple Months
65
+
66
+ `number-of-months` renders more than one grid at a time — useful when ranges routinely cross a month boundary and paging back and forth hides one end of the selection. The grids stack vertically.
67
+
68
+ ```vue
69
+ <script setup>
70
+ import { ref } from 'vue'
71
+
72
+ const range = ref()
73
+ </script>
74
+
75
+ <template>
76
+ <TelaRangeCalendar v-model="range" :number-of-months="2" />
77
+ </template>
78
+ ```
79
+
80
+ ### Bounds and Unavailable Dates
81
+
82
+ `min-value` and `max-value` clamp what can be reached at all; `is-date-unavailable` rejects individual dates inside that window. Unavailable days render struck through in the destructive color, disabled days at reduced opacity.
83
+
84
+ ```vue
85
+ <script setup>
86
+ import { getLocalTimeZone, isWeekend, today } from '@internationalized/date'
87
+ import { ref } from 'vue'
88
+
89
+ const now = today(getLocalTimeZone())
90
+ const range = ref()
91
+
92
+ function isDateUnavailable(date) {
93
+ return isWeekend(date, 'en-US')
94
+ }
95
+ </script>
96
+
97
+ <template>
98
+ <TelaRangeCalendar
99
+ v-model="range"
100
+ :min-value="now"
101
+ :max-value="now.add({ months: 2 })"
102
+ :is-date-unavailable="isDateUnavailable"
103
+ />
104
+ </template>
105
+ ```
106
+
107
+ ### Locale
108
+
109
+ `locale` drives weekday names, the first day of the week, the month options in the header, and the segment order of the time fields (`HH:mm` vs. `h:mm AM/PM`).
110
+
111
+ ```vue
112
+ <script setup>
113
+ import { ref } from 'vue'
114
+
115
+ const range = ref()
116
+ </script>
117
+
118
+ <template>
119
+ <TelaRangeCalendar v-model="range" locale="pt-BR" with-time />
120
+ </template>
121
+ ```
122
+
123
+ ### Disabled
124
+
125
+ `disabled` freezes the whole calendar — grid, navigation, and the time fields, which drop to 50% opacity and stop accepting keystrokes.
126
+
127
+ ```vue
128
+ <script setup>
129
+ import { getLocalTimeZone, today } from '@internationalized/date'
130
+ import { ref } from 'vue'
131
+
132
+ const start = today(getLocalTimeZone())
133
+ const range = ref({ start, end: start.add({ days: 4 }) })
134
+ </script>
135
+
136
+ <template>
137
+ <TelaRangeCalendar v-model="range" disabled with-time />
138
+ </template>
139
+ ```
140
+
141
+ ## Rules
142
+
143
+ **`with-time` needs a bound `v-model`.** Time edits are applied to the *current* `modelValue` — with an uncontrolled calendar (`default-value` only) there is nothing to merge into, so changing a time silently does nothing to the value you read back. Bind `v-model` whenever `with-time` is set.
144
+
145
+ **Values are `DateValue`, not `Date`.** Build them with `@internationalized/date` and convert only at the boundary where you actually need a JS date:
146
+
147
+ ```vue
148
+ const jsDate = range.value.start.toDate(getLocalTimeZone())
149
+ ```
150
+
151
+ **Don't wrap it in another card.** The calendar already ships its own surface — `bg`, a `0.5px` border, and a `12px` radius. Nesting it inside a `TelaCard` or a bordered `div` produces a double frame.
152
+
153
+ **Anchor it in a popover, don't restyle it into one.** For a compact trigger-plus-calendar control, put `TelaRangeCalendar` inside a `TelaPopover` rather than overriding its padding and border to look like a dropdown.
154
+
155
+ **Days outside the visible month are hidden, not dimmed.** Leading and trailing cells render as `invisible` placeholders, so the grid keeps its shape but only in-month days are clickable. Don't add your own outside-day styling — it will never be seen.
@@ -0,0 +1,188 @@
1
+ <script lang="ts" setup>
2
+ import type { DateValue } from '@internationalized/date'
3
+ import type { DateRange, RangeCalendarRootEmits, RangeCalendarRootProps } from 'reka-ui'
4
+ import type { HTMLAttributes } from 'vue'
5
+ import { getLocalTimeZone, Time, toCalendarDateTime, today } from '@internationalized/date'
6
+ import { reactiveOmit } from '@vueuse/core'
7
+ import { RangeCalendarRoot, useForwardProps } from 'reka-ui'
8
+
9
+ const props = defineProps<RangeCalendarRootProps & {
10
+ class?: HTMLAttributes['class']
11
+ withTime?: boolean
12
+ }>()
13
+
14
+ const emits = defineEmits<RangeCalendarRootEmits>()
15
+
16
+ const delegatedProps = reactiveOmit(props, 'class', 'withTime')
17
+
18
+ const forwarded = useForwardProps(delegatedProps)
19
+
20
+ const placeholder = shallowRef<DateValue>(
21
+ props.placeholder
22
+ ?? props.defaultPlaceholder
23
+ ?? props.defaultValue?.start
24
+ ?? props.modelValue?.start
25
+ ?? today(getLocalTimeZone()),
26
+ )
27
+
28
+ function setPlaceholder(value: DateValue) {
29
+ placeholder.value = value
30
+ emits('update:placeholder', value)
31
+ }
32
+
33
+ const monthOptions = computed(() =>
34
+ Array.from({ length: 12 }, (_, index) => ({
35
+ value: String(index + 1),
36
+ label: new Intl.DateTimeFormat(props.locale, { month: 'long' }).format(new Date(2000, index, 1)).slice(0, 3),
37
+ })),
38
+ )
39
+
40
+ const yearOptions = computed(() =>
41
+ Array.from({ length: 21 }, (_, index) => {
42
+ const year = placeholder.value.year - 10 + index
43
+ return { value: String(year), label: String(year) }
44
+ }),
45
+ )
46
+
47
+ const selectedMonth = computed({
48
+ get: () => String(placeholder.value.month),
49
+ set: value => setPlaceholder(placeholder.value.set({ month: Number(value) })),
50
+ })
51
+
52
+ const selectedYear = computed({
53
+ get: () => String(placeholder.value.year),
54
+ set: value => setPlaceholder(placeholder.value.set({ year: Number(value) })),
55
+ })
56
+
57
+ function timeFromDate(date?: DateValue | null) {
58
+ if (date && 'hour' in date)
59
+ return new Time(date.hour, date.minute)
60
+
61
+ return undefined
62
+ }
63
+
64
+ const initialRange = props.modelValue ?? props.defaultValue
65
+
66
+ const startTime = shallowRef(timeFromDate(initialRange?.start) ?? new Time(0, 0))
67
+ const endTime = shallowRef(timeFromDate(initialRange?.end) ?? new Time(23, 59))
68
+
69
+ watch(() => props.modelValue, (value) => {
70
+ const start = timeFromDate(value?.start)
71
+ const end = timeFromDate(value?.end)
72
+
73
+ if (start)
74
+ startTime.value = start
75
+ if (end)
76
+ endTime.value = end
77
+ })
78
+
79
+ function applyTime(date: DateValue, time: Time): DateValue {
80
+ if ('hour' in date)
81
+ return date.set({ hour: time.hour, minute: time.minute })
82
+
83
+ return toCalendarDateTime(date, time)
84
+ }
85
+
86
+ function handleModelValueUpdate(value: DateRange) {
87
+ if (!props.withTime) {
88
+ emits('update:modelValue', value)
89
+ return
90
+ }
91
+
92
+ emits('update:modelValue', {
93
+ start: value.start ? applyTime(value.start, startTime.value) : value.start,
94
+ end: value.end ? applyTime(value.end, endTime.value) : value.end,
95
+ })
96
+ }
97
+
98
+ function setStartTime(time: Time) {
99
+ startTime.value = time
100
+
101
+ if (props.modelValue?.start)
102
+ emits('update:modelValue', { ...props.modelValue, start: applyTime(props.modelValue.start, time) })
103
+ }
104
+
105
+ function setEndTime(time: Time) {
106
+ endTime.value = time
107
+
108
+ if (props.modelValue?.end)
109
+ emits('update:modelValue', { ...props.modelValue, end: applyTime(props.modelValue.end, time) })
110
+ }
111
+ </script>
112
+
113
+ <template>
114
+ <RangeCalendarRoot
115
+ v-slot="{ grid, weekDays }"
116
+ data-slot="range-calendar"
117
+ v-bind="forwarded"
118
+ :placeholder="placeholder"
119
+ :class="cn('bg p-8px rounded-12px border-0.5px border', props.class)"
120
+ @update:placeholder="setPlaceholder"
121
+ @update:model-value="handleModelValueUpdate"
122
+ @update:start-value="emits('update:startValue', $event)"
123
+ >
124
+ <TelaRangeCalendarHeader>
125
+ <TelaRangeCalendarPrevButton />
126
+
127
+ <div flex="~ gap-4px" flex-1 mx-4px>
128
+ <TelaSelectMenu
129
+ v-model="selectedMonth"
130
+ :options="monthOptions"
131
+ class="min-w-0 flex-1"
132
+ trigger-class="w-full"
133
+ />
134
+ <TelaSelectMenu
135
+ v-model="selectedYear"
136
+ :options="yearOptions"
137
+ class="min-w-0 flex-[1.1]"
138
+ trigger-class="w-full"
139
+ />
140
+ </div>
141
+
142
+ <TelaRangeCalendarNextButton />
143
+ </TelaRangeCalendarHeader>
144
+
145
+ <div flex="~ col" pt-6px>
146
+ <TelaRangeCalendarGrid v-for="month in grid" :key="month.value.toString()">
147
+ <TelaRangeCalendarGridHead>
148
+ <TelaRangeCalendarGridRow>
149
+ <TelaRangeCalendarHeadCell v-for="day in weekDays" :key="day">
150
+ {{ day }}
151
+ </TelaRangeCalendarHeadCell>
152
+ </TelaRangeCalendarGridRow>
153
+ </TelaRangeCalendarGridHead>
154
+ <TelaRangeCalendarGridBody>
155
+ <TelaRangeCalendarGridRow
156
+ v-for="(weekDates, index) in month.rows"
157
+ :key="`weekDate-${index}`"
158
+ >
159
+ <TelaRangeCalendarCell
160
+ v-for="weekDate in weekDates"
161
+ :key="weekDate.toString()"
162
+ :date="weekDate"
163
+ >
164
+ <TelaRangeCalendarCellTrigger
165
+ :day="weekDate"
166
+ :month="month.value"
167
+ />
168
+ </TelaRangeCalendarCell>
169
+ </TelaRangeCalendarGridRow>
170
+ </TelaRangeCalendarGridBody>
171
+ </TelaRangeCalendarGrid>
172
+ </div>
173
+
174
+ <template v-if="props.withTime">
175
+ <div aria-label="Divider" class="w-[calc(100%+16px)]" ml--8px h-0.5px bg-border mt-6px mb-8px />
176
+
177
+ <TelaRangeCalendarTimeField
178
+ :start-time="startTime"
179
+ :end-time="endTime"
180
+ :disabled="props.disabled"
181
+ :locale="props.locale"
182
+ class="mt-4px"
183
+ @update:start-time="setStartTime"
184
+ @update:end-time="setEndTime"
185
+ />
186
+ </template>
187
+ </RangeCalendarRoot>
188
+ </template>
@@ -7,7 +7,7 @@ import { SelectIcon, SelectTrigger, useForwardProps } from 'reka-ui'
7
7
  const props = withDefaults(
8
8
  defineProps<SelectTriggerProps & { triggerIconSize?: string, class?: HTMLAttributes['class'] }>(),
9
9
  {
10
- triggerIconSize: '12px',
10
+ triggerIconSize: '14px',
11
11
  },
12
12
  )
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.60.0",
3
+ "version": "1.62.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",
@@ -62,7 +62,7 @@
62
62
  "ts-morph": "22.0.0",
63
63
  "typescript": "5.8.2",
64
64
  "unocss": "66.5.12",
65
- "vue": "3.5.13",
65
+ "vue": "3.5.17",
66
66
  "vue-component-meta": "3.0.8",
67
67
  "vue-docgen-api": "4.78.0",
68
68
  "vue-input-otp": "0.3.2",
@@ -1,119 +0,0 @@
1
- <script setup lang="ts">
2
- import { Icon } from '@iconify/vue'
3
- import { RangeCalendarCell, RangeCalendarCellTrigger, RangeCalendarGrid, RangeCalendarGridBody, RangeCalendarGridHead, RangeCalendarGridRow, RangeCalendarHeadCell, RangeCalendarHeader, RangeCalendarHeading, RangeCalendarNext, RangeCalendarPrev, RangeCalendarRoot } from 'radix-vue'
4
- import type { DateValue } from '@internationalized/date'
5
-
6
- defineProps<{
7
- maxValue?: DateValue
8
- isDateUnavailable?: (date: DateValue) => boolean
9
- }>()
10
- </script>
11
-
12
- <template>
13
- <RangeCalendarRoot
14
- v-slot="{ weekDays, grid }"
15
- bg-white
16
- rounded-xl
17
- p-4
18
- shadow-xl
19
- fixed-weeks
20
- :max-value="maxValue"
21
- :is-date-unavailable="isDateUnavailable"
22
- >
23
- <RangeCalendarHeader flex="~ items-center justify-between">
24
- <RangeCalendarPrev
25
- flex="~ items-center justify-center"
26
- cursor-pointer
27
- text-black
28
- rounded-9px
29
- bg-transparent
30
- w-8 h-8
31
- hover:bg-gray-200
32
- transition-all
33
- class="active:scale-98 focus:shadow-[0_0_0_2px] focus:shadow-black data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed data-[disabled]:hover:bg-transparent"
34
- >
35
- <Icon
36
- icon="radix-icons:chevron-left"
37
- w-6 h-6
38
- />
39
- </RangeCalendarPrev>
40
- <RangeCalendarHeading body-14-semibold text-black />
41
- <RangeCalendarNext
42
- flex="~ items-center justify-center"
43
- cursor-pointer
44
- text-black
45
- rounded-9px
46
- bg-transparent
47
- w-8 h-8
48
- hover:bg-gray-200
49
- transition-all
50
- class="active:scale-98 focus:shadow-[0_0_0_2px] focus:shadow-black data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed data-[disabled]:hover:bg-transparent"
51
- >
52
- <Icon
53
- icon="radix-icons:chevron-right"
54
- w-6 h-6
55
- />
56
- </RangeCalendarNext>
57
- </RangeCalendarHeader>
58
- <div
59
- flex="~ col"
60
- space-y-4
61
- pt-4
62
- sm:flex-row
63
- sm:space-x-4
64
- sm:space-y-0
65
- >
66
- <RangeCalendarGrid
67
- v-for="month in grid"
68
- :key="month.value.toString()"
69
- w-full
70
- border-collapse
71
- select-none
72
- space-y-1
73
- >
74
- <RangeCalendarGridHead>
75
- <RangeCalendarGridRow w-full flex="~ justify-between" mb-1>
76
- <RangeCalendarHeadCell
77
- v-for="day in weekDays"
78
- :key="day"
79
- w-8
80
- rounded-md
81
- body-12-semibold
82
- text-black
83
- >
84
- {{ day }}
85
- </RangeCalendarHeadCell>
86
- </RangeCalendarGridRow>
87
- </RangeCalendarGridHead>
88
- <RangeCalendarGridBody>
89
- <RangeCalendarGridRow
90
- v-for="(weekDates, index) in month.rows"
91
- :key="`weekDate-${index}`"
92
- flex="~ justify-between"
93
- w-full
94
- >
95
- <RangeCalendarCell
96
- v-for="weekDate in weekDates"
97
- :key="weekDate.toString()"
98
- :date="weekDate"
99
- >
100
- <RangeCalendarCellTrigger
101
- :day="weekDate"
102
- :month="month.value"
103
- relative
104
- flex="~ items-center justify-center"
105
- rounded-full
106
- whitespace-nowrap
107
- body-12-semibold
108
- text-black
109
- w-8 h-8
110
- outline-none
111
- class=" hover:bg-white! hover:shadow-[#2563eb] hover:shadow-[0_0_0_2px] data-[outside-view]:text-black/30 data-[selected]:!bg-blue-200 data-[selected]:text-[#1e3a8a]! hover:bg-blue-200 data-[highlighted]:bg-blue-200 data-[highlighted]:text-[#1e3a8a]! data-[unavailable]:pointer-events-none data-[unavailable]:text-gray-400! data-[unavailable]:bg-gray-50! data-[unavailable]:opacity-50 data-[unavailable]:cursor-not-allowed before:absolute before:top-[5px] before:hidden before:rounded-full before:w-1 before:h-1 before:bg-white data-[today]:before:block data-[today]:underline data-[today]:underline-offset-4"
112
- />
113
- </RangeCalendarCell>
114
- </RangeCalendarGridRow>
115
- </RangeCalendarGridBody>
116
- </RangeCalendarGrid>
117
- </div>
118
- </RangeCalendarRoot>
119
- </template>