add range calendar component

This commit is contained in:
pa
2026-02-12 12:12:01 +09:00
parent 77f1795697
commit d423406a28
14 changed files with 412 additions and 2 deletions

View File

@@ -42,8 +42,8 @@
"@kamiya4047/eslint-plugin-pretty-import": "^0.1.6",
"@sentry/vite-plugin": "^4.9.1",
"@sentry/vue": "^10.38.0",
"@sigma/node-border": "^3.0.0",
"@sigma/edge-curve": "^3.1.0",
"@sigma/node-border": "^3.0.0",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/vue-table": "^8.21.3",
"@tanstack/vue-virtual": "^3.13.18",
@@ -85,6 +85,7 @@
"tw-animate-css": "^1.4.0",
"vee-validate": "^4.15.1",
"vite": "^7.3.1",
"vitest": "^3.2.4",
"vue": "^3.5.28",
"vue-i18n": "^11.2.8",
"vue-json-pretty": "^2.6.0",
@@ -92,7 +93,6 @@
"vue-router": "^4.6.4",
"vue-showdown": "^4.2.0",
"vue-sonner": "^2.0.9",
"vitest": "^3.2.4",
"worker-timers": "^8.0.30",
"yargs": "^18.0.0",
"zod": "^3.25.76"

View File

@@ -0,0 +1,102 @@
<script setup>
import { RangeCalendarRoot, useForwardPropsEmits } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
import {
RangeCalendarCell,
RangeCalendarCellTrigger,
RangeCalendarGrid,
RangeCalendarGridBody,
RangeCalendarGridHead,
RangeCalendarGridRow,
RangeCalendarHeadCell,
RangeCalendarHeader,
RangeCalendarHeading,
RangeCalendarNextButton,
RangeCalendarPrevButton
} from '.';
const props = defineProps({
defaultPlaceholder: { type: null, required: false },
defaultValue: { type: Object, required: false },
modelValue: { type: [Object, null], required: false },
placeholder: { type: null, required: false },
allowNonContiguousRanges: { type: Boolean, required: false },
pagedNavigation: { type: Boolean, required: false },
preventDeselect: { type: Boolean, required: false },
maximumDays: { type: Number, required: false },
weekStartsOn: { type: Number, required: false },
weekdayFormat: { type: String, required: false },
calendarLabel: { type: String, required: false },
fixedWeeks: { type: Boolean, required: false },
maxValue: { type: null, required: false },
minValue: { type: null, required: false },
locale: { type: String, required: false },
numberOfMonths: { type: Number, required: false },
disabled: { type: Boolean, required: false },
readonly: { type: Boolean, required: false },
initialFocus: { type: Boolean, required: false },
isDateDisabled: { type: Function, required: false },
isDateUnavailable: { type: Function, required: false },
isDateHighlightable: { type: Function, required: false },
dir: { type: String, required: false },
nextPage: { type: Function, required: false },
prevPage: { type: Function, required: false },
disableDaysOutsideCurrentView: { type: Boolean, required: false },
fixedDate: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const emits = defineEmits([
'update:modelValue',
'update:validModelValue',
'update:placeholder',
'update:startValue'
]);
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<RangeCalendarRoot
v-slot="{ grid, weekDays }"
data-slot="range-calendar"
:class="cn('p-3', props.class)"
v-bind="forwarded">
<RangeCalendarHeader>
<RangeCalendarHeading />
<div class="flex items-center gap-1">
<RangeCalendarPrevButton />
<RangeCalendarNextButton />
</div>
</RangeCalendarHeader>
<div class="flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
<RangeCalendarGrid v-for="month in grid" :key="month.value.toString()">
<RangeCalendarGridHead>
<RangeCalendarGridRow>
<RangeCalendarHeadCell v-for="day in weekDays" :key="day">
{{ day }}
</RangeCalendarHeadCell>
</RangeCalendarGridRow>
</RangeCalendarGridHead>
<RangeCalendarGridBody>
<RangeCalendarGridRow
v-for="(weekDates, index) in month.rows"
:key="`weekDate-${index}`"
class="mt-2 w-full">
<RangeCalendarCell v-for="weekDate in weekDates" :key="weekDate.toString()" :date="weekDate">
<RangeCalendarCellTrigger :day="weekDate" :month="month.value" />
</RangeCalendarCell>
</RangeCalendarGridRow>
</RangeCalendarGridBody>
</RangeCalendarGrid>
</div>
</RangeCalendarRoot>
</template>

View File

@@ -0,0 +1,30 @@
<script setup>
import { RangeCalendarCell, useForwardProps } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
date: { type: null, required: true },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarCell
data-slot="range-calendar-cell"
:class="
cn(
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:bg-accent first:[&:has([data-selected])]:rounded-l-md last:[&:has([data-selected])]:rounded-r-md [&:has([data-selected][data-selection-end])]:rounded-r-md [&:has([data-selected][data-selection-start])]:rounded-l-md',
props.class
)
"
v-bind="forwardedProps">
<slot />
</RangeCalendarCell>
</template>

View File

@@ -0,0 +1,44 @@
<script setup>
import { RangeCalendarCellTrigger, useForwardProps } from 'reka-ui';
import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
day: { type: null, required: true },
month: { type: null, required: true },
asChild: { type: Boolean, required: false },
as: { type: null, required: false, default: 'button' },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarCellTrigger
data-slot="range-calendar-trigger"
:class="
cn(
buttonVariants({ variant: 'ghost' }),
'h-8 w-8 p-0 font-normal data-[selected]:opacity-100',
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
// Selection Start
'data-[selection-start]:bg-primary data-[selection-start]:text-primary-foreground data-[selection-start]:hover:bg-primary data-[selection-start]:hover:text-primary-foreground data-[selection-start]:focus:bg-primary data-[selection-start]:focus:text-primary-foreground',
// Selection End
'data-[selection-end]:bg-primary data-[selection-end]:text-primary-foreground data-[selection-end]:hover:bg-primary data-[selection-end]:hover:text-primary-foreground data-[selection-end]:focus:bg-primary data-[selection-end]:focus:text-primary-foreground',
// Outside months
'data-[outside-view]:text-muted-foreground',
// Disabled
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
// Unavailable
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
props.class
)
"
v-bind="forwardedProps">
<slot />
</RangeCalendarCellTrigger>
</template>

View File

@@ -0,0 +1,24 @@
<script setup>
import { RangeCalendarGrid, useForwardProps } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarGrid
data-slot="range-calendar-grid"
:class="cn('w-full border-collapse space-x-1', props.class)"
v-bind="forwardedProps">
<slot />
</RangeCalendarGrid>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
import { RangeCalendarGridBody } from 'reka-ui';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false }
});
</script>
<template>
<RangeCalendarGridBody data-slot="range-calendar-grid-body" v-bind="props">
<slot />
</RangeCalendarGridBody>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
import { RangeCalendarGridHead } from 'reka-ui';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false }
});
</script>
<template>
<RangeCalendarGridHead data-slot="range-calendar-grid-head" v-bind="props">
<slot />
</RangeCalendarGridHead>
</template>

View File

@@ -0,0 +1,21 @@
<script setup>
import { RangeCalendarGridRow, useForwardProps } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarGridRow data-slot="range-calendar-grid-row" :class="cn('flex', props.class)" v-bind="forwardedProps">
<slot />
</RangeCalendarGridRow>
</template>

View File

@@ -0,0 +1,24 @@
<script setup>
import { RangeCalendarHeadCell, useForwardProps } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarHeadCell
data-slot="range-calendar-head-cell"
:class="cn('w-8 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)"
v-bind="forwardedProps">
<slot />
</RangeCalendarHeadCell>
</template>

View File

@@ -0,0 +1,24 @@
<script setup>
import { RangeCalendarHeader, useForwardProps } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarHeader
data-slot="range-calendar-header"
:class="cn('flex justify-center pt-1 relative items-center w-full', props.class)"
v-bind="forwardedProps">
<slot />
</RangeCalendarHeader>
</template>

View File

@@ -0,0 +1,29 @@
<script setup>
import { RangeCalendarHeading, useForwardProps } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
defineSlots();
const delegatedProps = reactiveOmit(props, 'class');
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarHeading
v-slot="{ headingValue }"
data-slot="range-calendar-heading"
:class="cn('text-sm font-medium', props.class)"
v-bind="forwardedProps">
<slot :heading-value>
{{ headingValue }}
</slot>
</RangeCalendarHeading>
</template>

View File

@@ -0,0 +1,36 @@
<script setup>
import { RangeCalendarNext, useForwardProps } from 'reka-ui';
import { ChevronRight } from 'lucide-vue-next';
import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
nextPage: { type: Function, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarNext
data-slot="range-calendar-next-button"
:class="
cn(
buttonVariants({ variant: 'outline' }),
'absolute right-1',
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class
)
"
v-bind="forwardedProps">
<slot>
<ChevronRight class="size-4" />
</slot>
</RangeCalendarNext>
</template>

View File

@@ -0,0 +1,36 @@
<script setup>
import { RangeCalendarPrev, useForwardProps } from 'reka-ui';
import { ChevronLeft } from 'lucide-vue-next';
import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
prevPage: { type: Function, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarPrev
data-slot="range-calendar-prev-button"
:class="
cn(
buttonVariants({ variant: 'outline' }),
'absolute left-1',
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class
)
"
v-bind="forwardedProps">
<slot>
<ChevronLeft class="size-4" />
</slot>
</RangeCalendarPrev>
</template>

View File

@@ -0,0 +1,12 @@
export { default as RangeCalendar } from './RangeCalendar.vue';
export { default as RangeCalendarCell } from './RangeCalendarCell.vue';
export { default as RangeCalendarCellTrigger } from './RangeCalendarCellTrigger.vue';
export { default as RangeCalendarGrid } from './RangeCalendarGrid.vue';
export { default as RangeCalendarGridBody } from './RangeCalendarGridBody.vue';
export { default as RangeCalendarGridHead } from './RangeCalendarGridHead.vue';
export { default as RangeCalendarGridRow } from './RangeCalendarGridRow.vue';
export { default as RangeCalendarHeadCell } from './RangeCalendarHeadCell.vue';
export { default as RangeCalendarHeader } from './RangeCalendarHeader.vue';
export { default as RangeCalendarHeading } from './RangeCalendarHeading.vue';
export { default as RangeCalendarNextButton } from './RangeCalendarNextButton.vue';
export { default as RangeCalendarPrevButton } from './RangeCalendarPrevButton.vue';