feat: add v-calendar

This commit is contained in:
zernonia 2023-09-04 00:59:36 +08:00
parent bb7663330f
commit a0c08b2355
10 changed files with 395 additions and 68 deletions

View File

@ -133,13 +133,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/components/button',
items: [],
},
// {
// title: "Calendar",
// href: "#",
// label: "Soon",
// disabled: true,
// items: []
// },
{
title: 'Calendar',
href: '/docs/components/calendar',
items: [],
},
{
title: 'Card',
href: '/docs/components/card',
@ -179,13 +177,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/components/data-table',
items: [],
},
// {
// title: "Date Picker",
// href: "#",
// label: "Soon",
// disabled: true,
// items: []
// },
{
title: 'Date Picker',
href: '/docs/components/date-picker',
items: [],
},
{
title: 'Dialog',
href: '/docs/components/dialog',

View File

@ -16,8 +16,10 @@
"@vueuse/core": "^10.2.1",
"class-variance-authority": "^0.6.1",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
"lucide-vue-next": "^0.268.0",
"tailwindcss-animate": "^1.0.6",
"v-calendar": "^3.0.3",
"vitepress": "^1.0.0-rc.10",
"vue": "^3.3.4"
},

View File

@ -0,0 +1,44 @@
---
title: Calendar
description: A date field component that allows users to enter and edit date.
---
<ComponentPreview name="CalendarDemo" >
<<< ../../../lib/registry/default/examples/CalendarDemo.vue
</ComponentPreview>
## About
The `Calendar` component is built on top of [VCalendar](https://vcalendar.io/getting-started/installation.html).
## Installation
```bash
npx shadcn-vue@latest add calendar
```
<ManualInstall>
1. Install `radix-vue`:
```bash
npm install radix-vue
```
2. Copy and paste the component source files linked at the top of this page into your project.
</ManualInstall>
## Usage
```vue
<script setup lang="ts">
import { Calendar } from '@/lib/registry/default/ui/calendar'
</script>
<template>
<Calendar />
</template>
```

View File

@ -0,0 +1,79 @@
---
title: Date Picker
description: A date picker component with range and presets.
---
<ComponentPreview name="DatePickerDemo" >
<<< ../../../lib/registry/default/examples/DatePickerDemo.vue
</ComponentPreview>
## Installation
The Date Picker is built using a composition of the `<Popover />` and the `<Calendar />` components.
See installation instructions for the [Popover](/docs/components/popover#installation) and the [Calendar](/docs/components/calendar#installation) components.
## Usage
```vue
<script setup lang="ts">
import { format } from 'date-fns'
import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { ref } from 'vue'
import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/default/ui/button'
import { Calendar } from '@/lib/registry/default/ui/calendar'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/default/ui/popover'
const date = ref<Date>()
</script>
<template>
<Popover>
<PopoverTrigger as-child>
<Button
:variant="'outline'"
:class="cn(
'w-[280px] justify-start text-left font-normal',
!date && 'text-muted-foreground',
)"
>
<CalendarIcon class="mr-2 h-4 w-4" />
<span>{{ date ? format(date, "PPP") : "Pick a date" }}</span>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Calendar v-model="date" />
</PopoverContent>
</Popover>
</template>
```
## Examples
### Date Range Picker
<ComponentPreview name="DatePickerWithRange" >
<<< ../../../lib/registry/default/examples/DatePickerWithRange.vue
</ComponentPreview>
### Date Picker
<ComponentPreview name="DatePickerDemo" >
<<< ../../../lib/registry/default/examples/DatePickerDemo.vue
</ComponentPreview>

View File

@ -0,0 +1,10 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Calendar } from '@/lib/registry/default/ui/calendar'
const date = ref(new Date())
</script>
<template>
<Calendar v-model="date" class="rounded-md border" />
</template>

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import { format } from 'date-fns'
import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { ref } from 'vue'
import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/default/ui/button'
import { Calendar } from '@/lib/registry/default/ui/calendar'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/default/ui/popover'
const date = ref<Date>()
</script>
<template>
<Popover>
<PopoverTrigger as-child>
<Button
:variant="'outline'"
:class="cn(
'w-[280px] justify-start text-left font-normal',
!date && 'text-muted-foreground',
)"
>
<CalendarIcon class="mr-2 h-4 w-4" />
<span>{{ date ? format(date, "PPP") : "Pick a date" }}</span>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Calendar v-model="date" />
</PopoverContent>
</Popover>
</template>

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import { addDays, format } from 'date-fns'
import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { ref } from 'vue'
import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/default/ui/button'
import { Calendar } from '@/lib/registry/default/ui/calendar'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/default/ui/popover'
const date = ref({
start: new Date(2022, 0, 20),
end: addDays(new Date(2022, 0, 20), 20),
})
</script>
<template>
<div :class="cn('grid gap-2', $attrs.class ?? '')">
<Popover>
<PopoverTrigger as-child>
<Button
id="date"
:variant="'outline'"
:class="cn(
'w-[300px] justify-start text-left font-normal',
!date && 'text-muted-foreground',
)"
>
<CalendarIcon class="mr-2 h-4 w-4" />
<span>
{{ date.start ? (
date.end ? `${format(date.start, 'LLL dd, y')} - ${format(date.end, 'LLL dd, y')}`
: format(date.start, 'LLL dd, y')
) : 'Pick a date' }}
</span>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0" align="start" :avoid-collisions="true">
<Calendar
v-model.range="date"
:columns="2"
/>
</PopoverContent>
</Popover>
</div>
</template>

View File

@ -1,63 +1,110 @@
<script setup lang="ts">
import { useDark } from '@vueuse/core'
import { Calendar } from 'v-calendar'
import 'v-calendar/style.css'
import { useVModel } from '@vueuse/core'
import type { Calendar } from 'v-calendar'
import { DatePicker } from 'v-calendar'
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
import { computed, nextTick, onMounted, ref } from 'vue'
import { buttonVariants } from '../button'
import { cn } from '@/lib/utils'
const isDark = useDark()
const props = withDefaults(defineProps< {
modelValue?: string | number | Date | Partial<{
start: Date
end: Date
}>
modelModifiers?: object
columns?: number
type?: 'single' | 'range'
}>(), {
type: 'single',
columns: 1,
})
const emits = defineEmits<{
(e: 'update:modelValue', payload: typeof props.modelValue): void
}>()
const modelValue = useVModel(props, 'modelValue', emits, {
passive: true,
})
const datePicker = ref<InstanceType<typeof DatePicker>>()
// @ts-expect-error in this current version of v-calendar has the calendaRef instance, which is required to handle arrow nav.
const calendarRef = computed<InstanceType<typeof Calendar>>(() => datePicker.value.calendarRef)
function handleNav(direction: 'prev' | 'next') {
if (!calendarRef.value)
return
if (direction === 'prev')
calendarRef.value.movePrev()
else calendarRef.value.moveNext()
}
onMounted(async () => {
await nextTick()
await nextTick()
if (modelValue.value instanceof Date && calendarRef.value)
calendarRef.value.focusDate(modelValue.value)
})
</script>
<template>
<Calendar :is-dark="isDark" borderless trim-weeks expanded />
<div class="relative">
<div class="absolute top-3 flex justify-between w-full px-4">
<button :class="cn(buttonVariants({ variant: 'outline' }), 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100')" @click="handleNav('prev')">
<ChevronLeft class="w-4 h-4" />
</button>
<button :class="cn(buttonVariants({ variant: 'outline' }), 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100')" @click="handleNav('next')">
<ChevronRight class="w-4 h-4" />
</button>
</div>
<DatePicker ref="datePicker" v-model="modelValue" :model-modifiers="modelModifiers" class="calendar" trim-weeks :transition="'none'" :columns="columns" />
</div>
</template>
<style>
:root {
--vc-font-family: "Inter", sans-serif;
--vc-rounded-full: var(--radius);
--vc-font-bold: 500;
--vc-font-semibold: 600;
--vc-text-lg: 14px;
<style lang="postcss">
.calendar {
@apply p-3 text-center;
}
.vc-light,
.vc-dark {
--vc-bg: var(--background);
--vc-border: var(--border);
--vc-focus-ring: 0 0 0 3px rgba(0, 0, 0, 0.2);
--vc-weekday-color: var(--muted);
--vc-popover-content-color: var(--muted);
--vc-popover-content-bg: var(--background);
--vc-popover-content-border: var(--border);
&.vc-attr,
& .vc-attr {
--vc-content-color: var(--primary);
--vc-highlight-outline-bg: var(--primary);
--vc-highlight-outline-border: var(--primary);
--vc-highlight-outline-content-color: var(--primary-foreground);
--vc-highlight-light-bg: var(
--vc-accent-200
); /* Highlighted color between two dates */
--vc-highlight-light-content-color: var(--secondary-foreground);
--vc-highlight-solid-bg: var(--primary);
--vc-highlight-solid-content-color: var(--primary-foreground);
--vc-dot-bg: var(--primary);
--vc-bar-bg: var(--primary);
}
.calendar .vc-pane-layout {
@apply grid gap-4;
}
.vc-blue {
--vc-accent-200: var(--secondary);
--vc-accent-400: var(--primary);
--vc-accent-500: var(--primary);
--vc-accent-600: var(--primary);
.calendar .vc-title {
@apply text-sm font-medium pointer-events-none;
}
.dark {
.vc-blue {
--vc-accent-200: var(--secondary);
--vc-accent-400: var(--primary);
--vc-accent-500: var(--secondary);
}
.calendar .vc-pane-header-wrapper {
@apply hidden;
}
.calendar .vc-weeks {
@apply mt-4;
}
.calendar .vc-weekdays {
@apply flex;
}
.calendar .vc-weekday {
@apply text-muted-foreground rounded-md w-9 font-normal text-[0.8rem];
}
.calendar .vc-weeks {
@apply w-full space-y-2 flex flex-col [&>_div]:grid [&>_div]:grid-cols-7;
}
.calendar .vc-day:has(.vc-highlights) {
@apply bg-accent first:rounded-l-md last:rounded-r-md overflow-hidden;
}
.calendar .vc-day-content {
@apply text-center text-sm p-0 relative focus-within:relative focus-within:z-20 inline-flex items-center justify-center ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 hover:bg-accent hover:text-accent-foreground h-9 w-9 font-normal aria-selected:opacity-100 select-none;
}
.calendar .vc-day-content:not(.vc-highlight-content-light) {
@apply rounded-md;
}
.calendar .is-not-in-month:not(:has(.vc-highlight-content-solid)):not(:has(.vc-highlight-content-light)):not(:has(.vc-highlight-content-outline)),
.calendar .vc-disabled {
@apply text-muted-foreground opacity-50;
}
.calendar .vc-highlight-content-solid, .calendar .vc-highlight-content-outline {
@apply bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground;
}
.calendar .vc-highlight-content-light {
@apply bg-accent text-accent-foreground;
}
</style>

View File

@ -10,7 +10,7 @@ const props = defineProps<TabsTriggerProps & { class?: string }>()
v-bind="props"
:class="
cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none disabled:cursor-default disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow',
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
props.class,
)
"

View File

@ -56,12 +56,18 @@ importers:
clsx:
specifier: ^2.0.0
version: 2.0.0
date-fns:
specifier: ^2.30.0
version: 2.30.0
lucide-vue-next:
specifier: ^0.268.0
version: 0.268.0(vue@3.3.4)
tailwindcss-animate:
specifier: ^1.0.6
version: 1.0.6(tailwindcss@3.3.2)
v-calendar:
specifier: ^3.0.3
version: 3.0.3(@popperjs/core@2.11.8)(vue@3.3.4)
vitepress:
specifier: ^1.0.0-rc.10
version: 1.0.0-rc.10(@algolia/client-search@4.19.1)(@types/node@20.5.7)(search-insights@2.7.0)
@ -818,6 +824,13 @@ packages:
'@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.22.11)
dev: false
/@babel/runtime@7.22.11:
resolution: {integrity: sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.0
dev: false
/@babel/standalone@7.22.10:
resolution: {integrity: sha512-VmK2sWxUTfDDh9mPfCtFJPIehZToteqK+Zpwq8oJUjJ+WeeKIFTTQIrDzH7jEdom+cAaaguU7FI/FBsBWFkIeQ==}
engines: {node: '>=6.9.0'}
@ -1729,6 +1742,10 @@ packages:
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
dev: true
/@popperjs/core@2.11.8:
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
dev: false
/@rollup/pluginutils@5.0.3:
resolution: {integrity: sha512-hfllNN4a80rwNQ9QCxhxuHCGHMAvabXqxNdaChUSSadMre7t4iEUI6fFAhBOn/eIYTgYVhBv7vCLsAJ4u3lf3g==}
engines: {node: '>=14.0.0'}
@ -1840,7 +1857,6 @@ packages:
/@types/lodash@4.14.197:
resolution: {integrity: sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g==}
dev: true
/@types/mdast@3.0.12:
resolution: {integrity: sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg==}
@ -1869,6 +1885,10 @@ packages:
kleur: 3.0.3
dev: true
/@types/resize-observer-browser@0.1.7:
resolution: {integrity: sha512-G9eN0Sn0ii9PWQ3Vl72jDPgeJwRWhv2Qk/nQkJuWmRmOB4HX3/BhD5SE1dZs/hzPZL/WKnvF0RHdTSG54QJFyg==}
dev: false
/@types/semver@7.5.0:
resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==}
dev: true
@ -2977,6 +2997,21 @@ packages:
engines: {node: '>= 12'}
dev: false
/date-fns-tz@1.3.8(date-fns@2.30.0):
resolution: {integrity: sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==}
peerDependencies:
date-fns: '>=2.0.0'
dependencies:
date-fns: 2.30.0
dev: false
/date-fns@2.30.0:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'}
dependencies:
'@babel/runtime': 7.22.11
dev: false
/de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
dev: true
@ -4727,7 +4762,6 @@ packages:
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: true
/log-symbols@5.1.0:
resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==}
@ -5535,6 +5569,10 @@ packages:
strip-indent: 3.0.0
dev: true
/regenerator-runtime@0.14.0:
resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
dev: false
/regexp-tree@0.1.27:
resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
hasBin: true
@ -6497,6 +6535,22 @@ packages:
which-typed-array: 1.1.11
dev: false
/v-calendar@3.0.3(@popperjs/core@2.11.8)(vue@3.3.4):
resolution: {integrity: sha512-Skpp/nMoFqFadm94aWj0oOfazoux5T5Ug3/pbRbdolkoDrnVcL7Ronw1/SGFRUPGOwnLdYwhKPhrhSE1segW6w==}
peerDependencies:
'@popperjs/core': ^2.0.0
vue: ^3.2.0
dependencies:
'@popperjs/core': 2.11.8
'@types/lodash': 4.14.197
'@types/resize-observer-browser': 0.1.7
date-fns: 2.30.0
date-fns-tz: 1.3.8(date-fns@2.30.0)
lodash: 4.17.21
vue: 3.3.4
vue-screen-utils: 1.0.0-beta.13(vue@3.3.4)
dev: false
/v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
@ -6814,6 +6868,14 @@ packages:
- supports-color
dev: true
/vue-screen-utils@1.0.0-beta.13(vue@3.3.4):
resolution: {integrity: sha512-EJ/8TANKhFj+LefDuOvZykwMr3rrLFPLNb++lNBqPOpVigT2ActRg6icH9RFQVm4nHwlHIHSGm5OY/Clar9yIg==}
peerDependencies:
vue: ^3.2.0
dependencies:
vue: 3.3.4
dev: false
/vue-template-compiler@2.7.14:
resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==}
dependencies: