feat: add Calendar, Popover, and NativeSelect components with associated subcomponents
This commit is contained in:
parent
fa240261ec
commit
ccc857c48d
162
app/components/ui/calendar/Calendar.vue
Normal file
162
app/components/ui/calendar/Calendar.vue
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CalendarRootEmits, CalendarRootProps, DateValue } from "reka-ui"
|
||||||
|
import type { HTMLAttributes, Ref } from "vue"
|
||||||
|
import type { LayoutTypes } from "."
|
||||||
|
import { getLocalTimeZone, today } from "@internationalized/date"
|
||||||
|
import { createReusableTemplate, reactiveOmit, useVModel } from "@vueuse/core"
|
||||||
|
import { CalendarRoot, useDateFormatter, useForwardPropsEmits } from "reka-ui"
|
||||||
|
import { createYear, createYearRange, toDate } from "reka-ui/date"
|
||||||
|
import { computed, toRaw } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||||
|
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNextButton, CalendarPrevButton } from "."
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<CalendarRootProps & { class?: HTMLAttributes["class"], layout?: LayoutTypes, yearRange?: DateValue[] }>(), {
|
||||||
|
modelValue: undefined,
|
||||||
|
layout: undefined,
|
||||||
|
})
|
||||||
|
const emits = defineEmits<CalendarRootEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class", "layout", "placeholder")
|
||||||
|
|
||||||
|
const placeholder = useVModel(props, "placeholder", emits, {
|
||||||
|
passive: true,
|
||||||
|
defaultValue: props.defaultPlaceholder ?? today(getLocalTimeZone()),
|
||||||
|
}) as Ref<DateValue>
|
||||||
|
|
||||||
|
const formatter = useDateFormatter(props.locale ?? "en")
|
||||||
|
|
||||||
|
const yearRange = computed(() => {
|
||||||
|
return props.yearRange ?? createYearRange({
|
||||||
|
start: props?.minValue ?? (toRaw(props.placeholder) ?? props.defaultPlaceholder ?? today(getLocalTimeZone()))
|
||||||
|
.cycle("year", -100),
|
||||||
|
|
||||||
|
end: props?.maxValue ?? (toRaw(props.placeholder) ?? props.defaultPlaceholder ?? today(getLocalTimeZone()))
|
||||||
|
.cycle("year", 10),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const [DefineMonthTemplate, ReuseMonthTemplate] = createReusableTemplate<{ date: DateValue }>()
|
||||||
|
const [DefineYearTemplate, ReuseYearTemplate] = createReusableTemplate<{ date: DateValue }>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DefineMonthTemplate v-slot="{ date }">
|
||||||
|
<div class="**:data-[slot=native-select-icon]:right-1">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none">
|
||||||
|
{{ formatter.custom(toDate(date), { month: 'short' }) }}
|
||||||
|
</div>
|
||||||
|
<NativeSelect
|
||||||
|
class="text-xs h-8 pr-6 pl-2 text-transparent relative"
|
||||||
|
:model-value="date.month"
|
||||||
|
@change="(e: Event) => {
|
||||||
|
placeholder = placeholder.set({
|
||||||
|
month: Number((e?.target as any)?.value),
|
||||||
|
})
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<NativeSelectOption v-for="(month) in createYear({ dateObj: date })" :key="month.toString()" :value="month.month" :selected="date.month === month.month">
|
||||||
|
{{ formatter.custom(toDate(month), { month: 'short' }) }}
|
||||||
|
</NativeSelectOption>
|
||||||
|
</NativeSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefineMonthTemplate>
|
||||||
|
|
||||||
|
<DefineYearTemplate v-slot="{ date }">
|
||||||
|
<div class="**:data-[slot=native-select-icon]:right-1">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none">
|
||||||
|
{{ formatter.custom(toDate(date), { year: 'numeric' }) }}
|
||||||
|
</div>
|
||||||
|
<NativeSelect
|
||||||
|
class="text-xs h-8 pr-6 pl-2 text-transparent relative"
|
||||||
|
:model-value="date.year"
|
||||||
|
@change="(e: Event) => {
|
||||||
|
placeholder = placeholder.set({
|
||||||
|
year: Number((e?.target as any)?.value),
|
||||||
|
})
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<NativeSelectOption v-for="(year) in yearRange" :key="year.toString()" :value="year.year" :selected="date.year === year.year">
|
||||||
|
{{ formatter.custom(toDate(year), { year: 'numeric' }) }}
|
||||||
|
</NativeSelectOption>
|
||||||
|
</NativeSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefineYearTemplate>
|
||||||
|
|
||||||
|
<CalendarRoot
|
||||||
|
v-slot="{ grid, weekDays, date }"
|
||||||
|
v-bind="forwarded"
|
||||||
|
v-model:placeholder="placeholder"
|
||||||
|
data-slot="calendar"
|
||||||
|
:class="cn('p-3', props.class)"
|
||||||
|
>
|
||||||
|
<CalendarHeader class="pt-0">
|
||||||
|
<nav class="flex items-center gap-1 absolute top-0 inset-x-0 justify-between">
|
||||||
|
<CalendarPrevButton>
|
||||||
|
<slot name="calendar-prev-icon" />
|
||||||
|
</CalendarPrevButton>
|
||||||
|
<CalendarNextButton>
|
||||||
|
<slot name="calendar-next-icon" />
|
||||||
|
</CalendarNextButton>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<slot name="calendar-heading" :date="date" :month="ReuseMonthTemplate" :year="ReuseYearTemplate">
|
||||||
|
<template v-if="layout === 'month-and-year'">
|
||||||
|
<div class="flex items-center justify-center gap-1">
|
||||||
|
<ReuseMonthTemplate :date="date" />
|
||||||
|
<ReuseYearTemplate :date="date" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="layout === 'month-only'">
|
||||||
|
<div class="flex items-center justify-center gap-1">
|
||||||
|
<ReuseMonthTemplate :date="date" />
|
||||||
|
{{ formatter.custom(toDate(date), { year: 'numeric' }) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="layout === 'year-only'">
|
||||||
|
<div class="flex items-center justify-center gap-1">
|
||||||
|
{{ formatter.custom(toDate(date), { month: 'short' }) }}
|
||||||
|
<ReuseYearTemplate :date="date" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<CalendarHeading />
|
||||||
|
</template>
|
||||||
|
</slot>
|
||||||
|
</CalendarHeader>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
|
||||||
|
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
|
||||||
|
<CalendarGridHead>
|
||||||
|
<CalendarGridRow>
|
||||||
|
<CalendarHeadCell
|
||||||
|
v-for="day in weekDays" :key="day"
|
||||||
|
>
|
||||||
|
{{ day }}
|
||||||
|
</CalendarHeadCell>
|
||||||
|
</CalendarGridRow>
|
||||||
|
</CalendarGridHead>
|
||||||
|
<CalendarGridBody>
|
||||||
|
<CalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="mt-2 w-full">
|
||||||
|
<CalendarCell
|
||||||
|
v-for="weekDate in weekDates"
|
||||||
|
:key="weekDate.toString()"
|
||||||
|
:date="weekDate"
|
||||||
|
>
|
||||||
|
<CalendarCellTrigger
|
||||||
|
:day="weekDate"
|
||||||
|
:month="month.value"
|
||||||
|
/>
|
||||||
|
</CalendarCell>
|
||||||
|
</CalendarGridRow>
|
||||||
|
</CalendarGridBody>
|
||||||
|
</CalendarGrid>
|
||||||
|
</div>
|
||||||
|
</CalendarRoot>
|
||||||
|
</template>
|
||||||
23
app/components/ui/calendar/CalendarCell.vue
Normal file
23
app/components/ui/calendar/CalendarCell.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CalendarCellProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { CalendarCell, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarCell
|
||||||
|
data-slot="calendar-cell"
|
||||||
|
:class="cn('relative p-0 text-center text-sm focus-within:relative focus-within:z-20 flex-1 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent', props.class)"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CalendarCell>
|
||||||
|
</template>
|
||||||
39
app/components/ui/calendar/CalendarCellTrigger.vue
Normal file
39
app/components/ui/calendar/CalendarCellTrigger.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CalendarCellTriggerProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { CalendarCellTrigger, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes["class"] }>(), {
|
||||||
|
as: "button",
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarCellTrigger
|
||||||
|
data-slot="calendar-cell-trigger"
|
||||||
|
:class="cn(
|
||||||
|
buttonVariants({ variant: 'ghost' }),
|
||||||
|
'size-8 p-0 font-normal aria-selected:opacity-100 cursor-default',
|
||||||
|
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
|
||||||
|
// Selected
|
||||||
|
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 [&[data-selected]:hover]:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
|
||||||
|
// Disabled
|
||||||
|
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
|
||||||
|
// Unavailable
|
||||||
|
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
|
||||||
|
// Outside months
|
||||||
|
'data-[outside-view]:text-muted-foreground',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CalendarCellTrigger>
|
||||||
|
</template>
|
||||||
23
app/components/ui/calendar/CalendarGrid.vue
Normal file
23
app/components/ui/calendar/CalendarGrid.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CalendarGridProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { CalendarGrid, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarGrid
|
||||||
|
data-slot="calendar-grid"
|
||||||
|
:class="cn('w-full border-collapse space-x-1', props.class)"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CalendarGrid>
|
||||||
|
</template>
|
||||||
15
app/components/ui/calendar/CalendarGridBody.vue
Normal file
15
app/components/ui/calendar/CalendarGridBody.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CalendarGridBodyProps } from "reka-ui"
|
||||||
|
import { CalendarGridBody } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<CalendarGridBodyProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarGridBody
|
||||||
|
data-slot="calendar-grid-body"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CalendarGridBody>
|
||||||
|
</template>
|
||||||
16
app/components/ui/calendar/CalendarGridHead.vue
Normal file
16
app/components/ui/calendar/CalendarGridHead.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CalendarGridHeadProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { CalendarGridHead } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<CalendarGridHeadProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarGridHead
|
||||||
|
data-slot="calendar-grid-head"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CalendarGridHead>
|
||||||
|
</template>
|
||||||
22
app/components/ui/calendar/CalendarGridRow.vue
Normal file
22
app/components/ui/calendar/CalendarGridRow.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CalendarGridRowProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { CalendarGridRow, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarGridRow
|
||||||
|
data-slot="calendar-grid-row"
|
||||||
|
:class="cn('flex', props.class)" v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CalendarGridRow>
|
||||||
|
</template>
|
||||||
23
app/components/ui/calendar/CalendarHeadCell.vue
Normal file
23
app/components/ui/calendar/CalendarHeadCell.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CalendarHeadCellProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { CalendarHeadCell, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarHeadCell
|
||||||
|
data-slot="calendar-head-cell"
|
||||||
|
:class="cn('text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem]', props.class)"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CalendarHeadCell>
|
||||||
|
</template>
|
||||||
23
app/components/ui/calendar/CalendarHeader.vue
Normal file
23
app/components/ui/calendar/CalendarHeader.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CalendarHeaderProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { CalendarHeader, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarHeader
|
||||||
|
data-slot="calendar-header"
|
||||||
|
:class="cn('flex justify-center pt-1 relative items-center w-full px-8', props.class)"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CalendarHeader>
|
||||||
|
</template>
|
||||||
30
app/components/ui/calendar/CalendarHeading.vue
Normal file
30
app/components/ui/calendar/CalendarHeading.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CalendarHeadingProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { CalendarHeading, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<CalendarHeadingProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
defineSlots<{
|
||||||
|
default: (props: { headingValue: string }) => any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarHeading
|
||||||
|
v-slot="{ headingValue }"
|
||||||
|
data-slot="calendar-heading"
|
||||||
|
:class="cn('text-sm font-medium', props.class)"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot :heading-value>
|
||||||
|
{{ headingValue }}
|
||||||
|
</slot>
|
||||||
|
</CalendarHeading>
|
||||||
|
</template>
|
||||||
31
app/components/ui/calendar/CalendarNextButton.vue
Normal file
31
app/components/ui/calendar/CalendarNextButton.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CalendarNextProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { ChevronRight } from "@lucide/vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { CalendarNext, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
|
|
||||||
|
const props = defineProps<CalendarNextProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarNext
|
||||||
|
data-slot="calendar-next-button"
|
||||||
|
:class="cn(
|
||||||
|
buttonVariants({ variant: 'outline' }),
|
||||||
|
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronRight class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</CalendarNext>
|
||||||
|
</template>
|
||||||
31
app/components/ui/calendar/CalendarPrevButton.vue
Normal file
31
app/components/ui/calendar/CalendarPrevButton.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CalendarPrevProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { ChevronLeft } from "@lucide/vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { CalendarPrev, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
|
|
||||||
|
const props = defineProps<CalendarPrevProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarPrev
|
||||||
|
data-slot="calendar-prev-button"
|
||||||
|
:class="cn(
|
||||||
|
buttonVariants({ variant: 'outline' }),
|
||||||
|
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronLeft class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</CalendarPrev>
|
||||||
|
</template>
|
||||||
14
app/components/ui/calendar/index.ts
Normal file
14
app/components/ui/calendar/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export { default as Calendar } from "./Calendar.vue"
|
||||||
|
export { default as CalendarCell } from "./CalendarCell.vue"
|
||||||
|
export { default as CalendarCellTrigger } from "./CalendarCellTrigger.vue"
|
||||||
|
export { default as CalendarGrid } from "./CalendarGrid.vue"
|
||||||
|
export { default as CalendarGridBody } from "./CalendarGridBody.vue"
|
||||||
|
export { default as CalendarGridHead } from "./CalendarGridHead.vue"
|
||||||
|
export { default as CalendarGridRow } from "./CalendarGridRow.vue"
|
||||||
|
export { default as CalendarHeadCell } from "./CalendarHeadCell.vue"
|
||||||
|
export { default as CalendarHeader } from "./CalendarHeader.vue"
|
||||||
|
export { default as CalendarHeading } from "./CalendarHeading.vue"
|
||||||
|
export { default as CalendarNextButton } from "./CalendarNextButton.vue"
|
||||||
|
export { default as CalendarPrevButton } from "./CalendarPrevButton.vue"
|
||||||
|
|
||||||
|
export type LayoutTypes = "month-and-year" | "month-only" | "year-only" | undefined
|
||||||
50
app/components/ui/native-select/NativeSelect.vue
Normal file
50
app/components/ui/native-select/NativeSelect.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { AcceptableValue } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { ChevronDownIcon } from "@lucide/vue"
|
||||||
|
import { reactiveOmit, useVModel } from "@vueuse/core"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue?: AcceptableValue | AcceptableValue[], class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": AcceptableValue
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const modelValue = useVModel(props, "modelValue", emit, {
|
||||||
|
passive: true,
|
||||||
|
defaultValue: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="group/native-select relative w-fit has-[select:disabled]:opacity-50"
|
||||||
|
data-slot="native-select-wrapper"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
v-bind="{ ...$attrs, ...delegatedProps }"
|
||||||
|
v-model="modelValue"
|
||||||
|
data-slot="native-select"
|
||||||
|
:class="cn(
|
||||||
|
'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed',
|
||||||
|
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3',
|
||||||
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</select>
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="text-muted-foreground pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 opacity-50 select-none"
|
||||||
|
aria-hidden="true"
|
||||||
|
data-slot="native-select-icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
12
app/components/ui/native-select/NativeSelectOptGroup.vue
Normal file
12
app/components/ui/native-select/NativeSelectOptGroup.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<optgroup data-slot="native-select-optgroup" :class="cn('bg-popover text-popover-foreground', props.class)">
|
||||||
|
<slot />
|
||||||
|
</optgroup>
|
||||||
|
</template>
|
||||||
12
app/components/ui/native-select/NativeSelectOption.vue
Normal file
12
app/components/ui/native-select/NativeSelectOption.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<option data-slot="native-select-option" :class="cn('bg-popover text-popover-foreground', props.class)">
|
||||||
|
<slot />
|
||||||
|
</option>
|
||||||
|
</template>
|
||||||
3
app/components/ui/native-select/index.ts
Normal file
3
app/components/ui/native-select/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { default as NativeSelect } from "./NativeSelect.vue"
|
||||||
|
export { default as NativeSelectOptGroup } from "./NativeSelectOptGroup.vue"
|
||||||
|
export { default as NativeSelectOption } from "./NativeSelectOption.vue"
|
||||||
19
app/components/ui/popover/Popover.vue
Normal file
19
app/components/ui/popover/Popover.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PopoverRootEmits, PopoverRootProps } from "reka-ui"
|
||||||
|
import { PopoverRoot, useForwardPropsEmits } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<PopoverRootProps>()
|
||||||
|
const emits = defineEmits<PopoverRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PopoverRoot
|
||||||
|
v-slot="slotProps"
|
||||||
|
data-slot="popover"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</PopoverRoot>
|
||||||
|
</template>
|
||||||
15
app/components/ui/popover/PopoverAnchor.vue
Normal file
15
app/components/ui/popover/PopoverAnchor.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PopoverAnchorProps } from "reka-ui"
|
||||||
|
import { PopoverAnchor } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<PopoverAnchorProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PopoverAnchor
|
||||||
|
data-slot="popover-anchor"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</PopoverAnchor>
|
||||||
|
</template>
|
||||||
45
app/components/ui/popover/PopoverContent.vue
Normal file
45
app/components/ui/popover/PopoverContent.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PopoverContentEmits, PopoverContentProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
PopoverContent,
|
||||||
|
PopoverPortal,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<PopoverContentProps & { class?: HTMLAttributes["class"] }>(),
|
||||||
|
{
|
||||||
|
align: "center",
|
||||||
|
sideOffset: 4,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const emits = defineEmits<PopoverContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PopoverPortal>
|
||||||
|
<PopoverContent
|
||||||
|
data-slot="popover-content"
|
||||||
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 max-w-(--reka-popover-content-available-width) rounded-md border p-4 shadow-md origin-(--reka-popover-content-transform-origin) outline-hidden',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</PopoverContent>
|
||||||
|
</PopoverPortal>
|
||||||
|
</template>
|
||||||
15
app/components/ui/popover/PopoverTrigger.vue
Normal file
15
app/components/ui/popover/PopoverTrigger.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PopoverTriggerProps } from "reka-ui"
|
||||||
|
import { PopoverTrigger } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<PopoverTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PopoverTrigger
|
||||||
|
data-slot="popover-trigger"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</PopoverTrigger>
|
||||||
|
</template>
|
||||||
4
app/components/ui/popover/index.ts
Normal file
4
app/components/ui/popover/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { default as Popover } from "./Popover.vue"
|
||||||
|
export { default as PopoverAnchor } from "./PopoverAnchor.vue"
|
||||||
|
export { default as PopoverContent } from "./PopoverContent.vue"
|
||||||
|
export { default as PopoverTrigger } from "./PopoverTrigger.vue"
|
||||||
Loading…
Reference in New Issue
Block a user