@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.
- package/components/tela/button/button-variants.ts +22 -0
- package/components/tela/button/button.vue +12 -50
- package/components/tela/confirm-button/confirm-button.vue +2 -4
- package/components/tela/date-range-picker.vue +4 -2
- package/components/tela/range-calendar/range-calendar-cell-trigger.vue +41 -0
- package/components/tela/range-calendar/range-calendar-cell.vue +26 -0
- package/components/tela/range-calendar/range-calendar-grid-body.vue +20 -0
- package/components/tela/range-calendar/range-calendar-grid-head.vue +20 -0
- package/components/tela/range-calendar/range-calendar-grid-row.vue +20 -0
- package/components/tela/range-calendar/range-calendar-grid.vue +22 -0
- package/components/tela/range-calendar/range-calendar-head-cell.vue +20 -0
- package/components/tela/range-calendar/range-calendar-header.vue +20 -0
- package/components/tela/range-calendar/range-calendar-heading.vue +22 -0
- package/components/tela/range-calendar/range-calendar-next-button.vue +30 -0
- package/components/tela/range-calendar/range-calendar-prev-button.vue +30 -0
- package/components/tela/range-calendar/range-calendar-time-field.vue +224 -0
- package/components/tela/range-calendar/range-calendar.mdx +155 -0
- package/components/tela/range-calendar/range-calendar.vue +188 -0
- package/components/tela/select-menu/select-menu-trigger.vue +1 -1
- package/package.json +2 -2
- package/components/tela/range-calendar.vue +0 -119
|
@@ -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' | '
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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:
|
|
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: '
|
|
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.
|
|
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.
|
|
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>
|