feat: new calendar

This commit is contained in:
Sadegh Barati 2024-03-24 00:05:07 +03:30
parent d34c620055
commit ffe9696547
165 changed files with 2176 additions and 991 deletions

View File

@ -19,7 +19,7 @@ import CardStats from '@/lib/registry/new-york/example/CardStats.vue'
import { import {
Card, Card,
} from '@/lib/registry/new-york/ui/card' } from '@/lib/registry/new-york/ui/card'
import { Calendar } from '@/lib/registry/new-york/ui/calendar' import { Calendar } from '@/lib/registry/new-york/ui/v-calendar'
const range = ref({ const range = ref({
start: startOfToday(), start: startOfToday(),
@ -73,3 +73,4 @@ const range = ref({
</div> </div>
</ThemingLayout> </ThemingLayout>
</template> </template>
@/lib/registry/new-york/ui/v-calendar

View File

@ -179,6 +179,7 @@ export const docsConfig: DocsConfig = {
title: 'Calendar', title: 'Calendar',
href: '/docs/components/calendar', href: '/docs/components/calendar',
items: [], items: [],
label: 'New',
}, },
{ {
title: 'Card', title: 'Card',
@ -221,11 +222,6 @@ export const docsConfig: DocsConfig = {
href: '/docs/components/data-table', href: '/docs/components/data-table',
items: [], items: [],
}, },
{
title: 'Date Picker',
href: '/docs/components/date-picker',
items: [],
},
{ {
title: 'Dialog', title: 'Dialog',
href: '/docs/components/dialog', href: '/docs/components/dialog',

View File

@ -192,6 +192,20 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/CalendarDemo.vue").then((m) => m.default), component: () => import("../src/lib/registry/default/example/CalendarDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/CalendarDemo.vue"], files: ["../src/lib/registry/default/example/CalendarDemo.vue"],
}, },
"CalendarForm": {
name: "CalendarForm",
type: "components:example",
registryDependencies: ["calendar","button","form","popover","toast","utils"],
component: () => import("../src/lib/registry/default/example/CalendarForm.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/CalendarForm.vue"],
},
"CalendarWithSelect": {
name: "CalendarWithSelect",
type: "components:example",
registryDependencies: [],
component: () => import("../src/lib/registry/default/example/CalendarWithSelect.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/CalendarWithSelect.vue"],
},
"CardChat": { "CardChat": {
name: "CardChat", name: "CardChat",
type: "components:example", type: "components:example",
@ -395,41 +409,6 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/DataTableDemoColumn.vue").then((m) => m.default), component: () => import("../src/lib/registry/default/example/DataTableDemoColumn.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DataTableDemoColumn.vue"], files: ["../src/lib/registry/default/example/DataTableDemoColumn.vue"],
}, },
"DatePickerDemo": {
name: "DatePickerDemo",
type: "components:example",
registryDependencies: ["utils","button","calendar","popover"],
component: () => import("../src/lib/registry/default/example/DatePickerDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DatePickerDemo.vue"],
},
"DatePickerForm": {
name: "DatePickerForm",
type: "components:example",
registryDependencies: ["utils","button","calendar","form","popover","toast"],
component: () => import("../src/lib/registry/default/example/DatePickerForm.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DatePickerForm.vue"],
},
"DatePickerWithPresets": {
name: "DatePickerWithPresets",
type: "components:example",
registryDependencies: ["utils","button","calendar","popover","select"],
component: () => import("../src/lib/registry/default/example/DatePickerWithPresets.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DatePickerWithPresets.vue"],
},
"DatePickerWithRange": {
name: "DatePickerWithRange",
type: "components:example",
registryDependencies: ["utils","button","calendar","popover"],
component: () => import("../src/lib/registry/default/example/DatePickerWithRange.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DatePickerWithRange.vue"],
},
"DateTimePickerDemo": {
name: "DateTimePickerDemo",
type: "components:example",
registryDependencies: ["utils","button","calendar","popover"],
component: () => import("../src/lib/registry/default/example/DateTimePickerDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DateTimePickerDemo.vue"],
},
"DialogCustomCloseButton": { "DialogCustomCloseButton": {
name: "DialogCustomCloseButton", name: "DialogCustomCloseButton",
type: "components:example", type: "components:example",
@ -654,13 +633,6 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/RadioGroupForm.vue").then((m) => m.default), component: () => import("../src/lib/registry/default/example/RadioGroupForm.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/RadioGroupForm.vue"], files: ["../src/lib/registry/default/example/RadioGroupForm.vue"],
}, },
"RangePickerWithSlot": {
name: "RangePickerWithSlot",
type: "components:example",
registryDependencies: ["utils","button","calendar","popover"],
component: () => import("../src/lib/registry/default/example/RangePickerWithSlot.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/RangePickerWithSlot.vue"],
},
"ResizableDemo": { "ResizableDemo": {
name: "ResizableDemo", name: "ResizableDemo",
type: "components:example", type: "components:example",
@ -1081,6 +1053,55 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/TypographyTable.vue").then((m) => m.default), component: () => import("../src/lib/registry/default/example/TypographyTable.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/TypographyTable.vue"], files: ["../src/lib/registry/default/example/TypographyTable.vue"],
}, },
"VCalendarDemo": {
name: "VCalendarDemo",
type: "components:example",
registryDependencies: ["v-calendar"],
component: () => import("../src/lib/registry/default/example/VCalendarDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/VCalendarDemo.vue"],
},
"VDatePickerDemo": {
name: "VDatePickerDemo",
type: "components:example",
registryDependencies: ["utils","button","v-calendar","popover"],
component: () => import("../src/lib/registry/default/example/VDatePickerDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/VDatePickerDemo.vue"],
},
"VDatePickerForm": {
name: "VDatePickerForm",
type: "components:example",
registryDependencies: ["utils","button","v-calendar","form","popover","toast"],
component: () => import("../src/lib/registry/default/example/VDatePickerForm.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/VDatePickerForm.vue"],
},
"VDatePickerWithPresets": {
name: "VDatePickerWithPresets",
type: "components:example",
registryDependencies: ["utils","button","v-calendar","popover","select"],
component: () => import("../src/lib/registry/default/example/VDatePickerWithPresets.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/VDatePickerWithPresets.vue"],
},
"VDatePickerWithRange": {
name: "VDatePickerWithRange",
type: "components:example",
registryDependencies: ["utils","button","v-calendar","popover"],
component: () => import("../src/lib/registry/default/example/VDatePickerWithRange.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/VDatePickerWithRange.vue"],
},
"VDateTimePickerDemo": {
name: "VDateTimePickerDemo",
type: "components:example",
registryDependencies: ["utils","button","v-calendar","popover"],
component: () => import("../src/lib/registry/default/example/VDateTimePickerDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/VDateTimePickerDemo.vue"],
},
"VRangePickerWithSlot": {
name: "VRangePickerWithSlot",
type: "components:example",
registryDependencies: ["utils","button","v-calendar","popover"],
component: () => import("../src/lib/registry/default/example/VRangePickerWithSlot.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/VRangePickerWithSlot.vue"],
},
"ActivityGoal": { "ActivityGoal": {
name: "ActivityGoal", name: "ActivityGoal",
type: "components:example", type: "components:example",
@ -1292,6 +1313,20 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/CalendarDemo.vue").then((m) => m.default), component: () => import("../src/lib/registry/new-york/example/CalendarDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/CalendarDemo.vue"], files: ["../src/lib/registry/new-york/example/CalendarDemo.vue"],
}, },
"CalendarForm": {
name: "CalendarForm",
type: "components:example",
registryDependencies: ["calendar","button","form","popover","toast","utils"],
component: () => import("../src/lib/registry/new-york/example/CalendarForm.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/CalendarForm.vue"],
},
"CalendarWithSelect": {
name: "CalendarWithSelect",
type: "components:example",
registryDependencies: [],
component: () => import("../src/lib/registry/new-york/example/CalendarWithSelect.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/CalendarWithSelect.vue"],
},
"CardChat": { "CardChat": {
name: "CardChat", name: "CardChat",
type: "components:example", type: "components:example",
@ -1495,41 +1530,6 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/DataTableDemoColumn.vue").then((m) => m.default), component: () => import("../src/lib/registry/new-york/example/DataTableDemoColumn.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DataTableDemoColumn.vue"], files: ["../src/lib/registry/new-york/example/DataTableDemoColumn.vue"],
}, },
"DatePickerDemo": {
name: "DatePickerDemo",
type: "components:example",
registryDependencies: ["utils","button","calendar","popover"],
component: () => import("../src/lib/registry/new-york/example/DatePickerDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DatePickerDemo.vue"],
},
"DatePickerForm": {
name: "DatePickerForm",
type: "components:example",
registryDependencies: ["utils","button","calendar","form","popover","toast"],
component: () => import("../src/lib/registry/new-york/example/DatePickerForm.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DatePickerForm.vue"],
},
"DatePickerWithPresets": {
name: "DatePickerWithPresets",
type: "components:example",
registryDependencies: ["utils","button","calendar","popover","select"],
component: () => import("../src/lib/registry/new-york/example/DatePickerWithPresets.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DatePickerWithPresets.vue"],
},
"DatePickerWithRange": {
name: "DatePickerWithRange",
type: "components:example",
registryDependencies: ["utils","button","calendar","popover"],
component: () => import("../src/lib/registry/new-york/example/DatePickerWithRange.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DatePickerWithRange.vue"],
},
"DateTimePickerDemo": {
name: "DateTimePickerDemo",
type: "components:example",
registryDependencies: ["utils","button","calendar","popover"],
component: () => import("../src/lib/registry/new-york/example/DateTimePickerDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DateTimePickerDemo.vue"],
},
"DialogCustomCloseButton": { "DialogCustomCloseButton": {
name: "DialogCustomCloseButton", name: "DialogCustomCloseButton",
type: "components:example", type: "components:example",
@ -1754,13 +1754,6 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/RadioGroupForm.vue").then((m) => m.default), component: () => import("../src/lib/registry/new-york/example/RadioGroupForm.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/RadioGroupForm.vue"], files: ["../src/lib/registry/new-york/example/RadioGroupForm.vue"],
}, },
"RangePickerWithSlot": {
name: "RangePickerWithSlot",
type: "components:example",
registryDependencies: ["utils","button","calendar","popover"],
component: () => import("../src/lib/registry/new-york/example/RangePickerWithSlot.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/RangePickerWithSlot.vue"],
},
"ResizableDemo": { "ResizableDemo": {
name: "ResizableDemo", name: "ResizableDemo",
type: "components:example", type: "components:example",
@ -2181,6 +2174,55 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/TypographyTable.vue").then((m) => m.default), component: () => import("../src/lib/registry/new-york/example/TypographyTable.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/TypographyTable.vue"], files: ["../src/lib/registry/new-york/example/TypographyTable.vue"],
}, },
"VCalendarDemo": {
name: "VCalendarDemo",
type: "components:example",
registryDependencies: ["v-calendar"],
component: () => import("../src/lib/registry/new-york/example/VCalendarDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/VCalendarDemo.vue"],
},
"VDatePickerDemo": {
name: "VDatePickerDemo",
type: "components:example",
registryDependencies: ["utils","button","v-calendar","popover"],
component: () => import("../src/lib/registry/new-york/example/VDatePickerDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/VDatePickerDemo.vue"],
},
"VDatePickerForm": {
name: "VDatePickerForm",
type: "components:example",
registryDependencies: ["utils","button","v-calendar","form","popover","toast"],
component: () => import("../src/lib/registry/new-york/example/VDatePickerForm.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/VDatePickerForm.vue"],
},
"VDatePickerWithPresets": {
name: "VDatePickerWithPresets",
type: "components:example",
registryDependencies: ["utils","button","v-calendar","popover","select"],
component: () => import("../src/lib/registry/new-york/example/VDatePickerWithPresets.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/VDatePickerWithPresets.vue"],
},
"VDatePickerWithRange": {
name: "VDatePickerWithRange",
type: "components:example",
registryDependencies: ["utils","button","v-calendar","popover"],
component: () => import("../src/lib/registry/new-york/example/VDatePickerWithRange.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/VDatePickerWithRange.vue"],
},
"VDateTimePickerDemo": {
name: "VDateTimePickerDemo",
type: "components:example",
registryDependencies: ["utils","button","v-calendar","popover"],
component: () => import("../src/lib/registry/new-york/example/VDateTimePickerDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/VDateTimePickerDemo.vue"],
},
"VRangePickerWithSlot": {
name: "VRangePickerWithSlot",
type: "components:example",
registryDependencies: ["utils","button","v-calendar","popover"],
component: () => import("../src/lib/registry/new-york/example/VRangePickerWithSlot.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/VRangePickerWithSlot.vue"],
},
"ActivityGoal": { "ActivityGoal": {
name: "ActivityGoal", name: "ActivityGoal",
type: "components:example", type: "components:example",

View File

@ -30,6 +30,7 @@
"embla-carousel": "^8.0.0", "embla-carousel": "^8.0.0",
"embla-carousel-autoplay": "^8.0.0", "embla-carousel-autoplay": "^8.0.0",
"embla-carousel-vue": "^8.0.0", "embla-carousel-vue": "^8.0.0",
"flat-internationalized-date": "^1.2.3",
"lucide-vue-next": "^0.359.0", "lucide-vue-next": "^0.359.0",
"magic-string": "^0.30.8", "magic-string": "^0.30.8",
"radix-vue": "^1.5.3", "radix-vue": "^1.5.3",

View File

@ -2,90 +2,31 @@
title: Calendar title: Calendar
description: A date field component that allows users to enter and edit date. description: A date field component that allows users to enter and edit date.
source: apps/www/src/lib/registry/default/ui/calendar source: apps/www/src/lib/registry/default/ui/calendar
primitive: https://vcalendar.io/ primitive: https://www.radix-vue.com/components/calendar.html
--- ---
<ComponentPreview name="CalendarDemo" /> <ComponentPreview name="CalendarDemo" />
## About ## About
The `Calendar` component is built on top of [VCalendar](https://vcalendar.io/getting-started/installation.html). The `<Calendar />` component is built on top of the [RadixVue Calendar](https://www.radix-vue.com/components/calendar.html) component, which uses the [flat-internationalized-date](https://github.com/epr3/flat-internationalized-date) package to handle dates.
If you're looking for a range calendar, check out the [Range Calendar](#asdasd) component.
## Installation ## Installation
<TabPreview name="CLI"> ```shell
<template #CLI>
```bash
npx shadcn-vue@latest add calendar npx shadcn-vue@latest add calendar
``` ```
</template>
<template #Manual> ## Datepicker
<Steps> You can use the `<Calendar />` component to build a date picker. See the [Date Picker](#asdasd) page for more information.
### Install the following dependency ## Examples
```bash ### Form
npm install v-calendar
```
### Copy and paste the following code into your project <ComponentPreview name="CalendarWithSelect" />
<<< @/lib/registry/default/ui/calendar/Calendar.vue <ComponentPreview name="CalendarForm" />
</Steps>
</template>
</TabPreview>
## Usage
```vue
<script setup lang="ts">
import { Calendar } from '@/components/ui/calendar'
</script>
<template>
<Calendar />
</template>
```
The API is essentially the same, i.e. props and slots. See the [VCalendar](https://vcalendar.io/getting-started/installation.html) documentation for more information.
### Slots
The slots available are [those currently supported](https://github.com/nathanreyes/v-calendar/blob/v3.1.2/src/components/Calendar/CalendarSlot.vue#L16-L28) by VCalendar, namely :
- `day-content`
- `day-popover`
- `dp-footer`
- `footer`
- `header-title-wrapper`
- `header-title`
- `header-prev-button`
- `header-next-button`
- `nav`
- `nav-prev-button`
- `nav-next-button`
- `page`
- `time-header`
Example using the `day-content` slot:
```vue
<script setup lang="ts">
import { Calendar } from '@/components/ui/calendar'
</script>
<template>
<Calendar>
<template #day-content="{ day, dayProps, dayEvents }">
<div v-bind="dayProps" v-on="dayEvents">
{{ day.label }}
</div>
</template>
</Calendar>
</template>
```

View File

@ -0,0 +1,91 @@
---
title: Calendar
description: A date field component that allows users to enter and edit date.
source: apps/www/src/lib/registry/default/ui/calendar
primitive: https://vcalendar.io/
---
<ComponentPreview name="VCalendarDemo" />
## About
The `Calendar` component is built on top of [VCalendar](https://vcalendar.io/getting-started/installation.html).
## Installation
<TabPreview name="CLI">
<template #CLI>
```bash
npx shadcn-vue@latest add calendar
```
</template>
<template #Manual>
<Steps>
### Install the following dependency
```bash
npm install v-calendar
```
### Copy and paste the following code into your project
<<< @/lib/registry/default/ui/v-calendar/Calendar.vue
</Steps>
</template>
</TabPreview>
## Usage
```vue
<script setup lang="ts">
import { Calendar } from '@/components/ui/v-calendar'
</script>
<template>
<Calendar />
</template>
```
The API is essentially the same, i.e. props and slots. See the [VCalendar](https://vcalendar.io/getting-started/installation.html) documentation for more information.
### Slots
The slots available are [those currently supported](https://github.com/nathanreyes/v-calendar/blob/v3.1.2/src/components/Calendar/CalendarSlot.vue#L16-L28) by VCalendar, namely :
- `day-content`
- `day-popover`
- `dp-footer`
- `footer`
- `header-title-wrapper`
- `header-title`
- `header-prev-button`
- `header-next-button`
- `nav`
- `nav-prev-button`
- `nav-next-button`
- `page`
- `time-header`
Example using the `day-content` slot:
```vue
<script setup lang="ts">
import { Calendar } from '@/components/ui/v-calendar'
</script>
<template>
<Calendar>
<template #day-content="{ day, dayProps, dayEvents }">
<div v-bind="dayProps" v-on="dayEvents">
{{ day.label }}
</div>
</template>
</Calendar>
</template>
```

View File

@ -3,7 +3,7 @@ title: Date Picker
description: A date picker component with range and presets. description: A date picker component with range and presets.
--- ---
<ComponentPreview name="DatePickerDemo" /> <ComponentPreview name="VDatePickerDemo" />
## Installation ## Installation
@ -21,7 +21,7 @@ import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { ref } from 'vue' import { ref } from 'vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar' import { Calendar } from '@/components/ui/v-calendar'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
@ -56,24 +56,24 @@ const date = ref<Date>()
### Date Picker ### Date Picker
<ComponentPreview name="DatePickerDemo" /> <ComponentPreview name="VDatePickerDemo" />
### Date Range Picker ### Date Range Picker
<ComponentPreview name="DatePickerWithRange" /> <ComponentPreview name="VDatePickerWithRange" />
### Date Time Picker ### Date Time Picker
<ComponentPreview name="DateTimePickerDemo" /> <ComponentPreview name="VDateTimePickerDemo" />
### With Presets ### With Presets
<ComponentPreview name="DatePickerWithPresets" /> <ComponentPreview name="VDatePickerWithPresets" />
### With Slot ### With Slot
<ComponentPreview name="RangePickerWithSlot" /> <ComponentPreview name="VRangePickerWithSlot" />
### Form ### Form
<ComponentPreview name="DatePickerForm" /> <ComponentPreview name="VDatePickerForm" />

View File

@ -5,7 +5,7 @@ import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { ref } from 'vue' import { ref } from 'vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { Calendar } from '@/lib/registry/new-york/ui/calendar' import { Calendar } from '@/lib/registry/new-york/ui/v-calendar'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
@ -49,3 +49,4 @@ const date = ref({
</Popover> </Popover>
</div> </div>
</template> </template>
@/lib/registry/new-york/ui/v-calendar

View File

@ -24,7 +24,7 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from '@/lib/registry/default/ui/popover' } from '@/lib/registry/default/ui/popover'
import { Calendar } from '@/lib/registry/new-york/ui/calendar' import { Calendar } from '@/lib/registry/new-york/ui/v-calendar'
import { toast } from '@/lib/registry/new-york/ui/toast' import { toast } from '@/lib/registry/new-york/ui/toast'
const open = ref(false) const open = ref(false)
@ -184,3 +184,4 @@ async function onSubmit(values: any) {
</div> </div>
</Form> </Form>
</template> </template>
@/lib/registry/new-york/ui/v-calendar

View File

@ -6,7 +6,7 @@ import addHours from 'date-fns/addHours'
import format from 'date-fns/format' import format from 'date-fns/format'
import nextSaturday from 'date-fns/nextSaturday' import nextSaturday from 'date-fns/nextSaturday'
import type { Mail } from '../data/mails' import type { Mail } from '../data/mails'
import { Calendar } from '@/lib/registry/new-york/ui/calendar' import { Calendar } from '@/lib/registry/new-york/ui/v-calendar'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/lib/registry/new-york/ui/dropdown-menu' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/lib/registry/new-york/ui/dropdown-menu'
import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/new-york/ui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/new-york/ui/popover'
import { Avatar, AvatarFallback } from '@/lib/registry/new-york/ui/avatar' import { Avatar, AvatarFallback } from '@/lib/registry/new-york/ui/avatar'

View File

@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { getLocalTimeZone, today } from 'flat-internationalized-date'
import { Calendar } from '@/lib/registry/default/ui/calendar' import { Calendar } from '@/lib/registry/default/ui/calendar'
const date = ref(new Date()) const value = ref(today(getLocalTimeZone()))
</script> </script>
<template> <template>
<Calendar v-model="date" class="rounded-md border" /> <Calendar v-model="value" :weekday-format="'short'" class="rounded-md border" />
</template> </template>

View File

@ -0,0 +1,95 @@
<script setup lang="ts">
import { h, ref } from 'vue'
import { DateFormatter, type DateValue, createCalendarDate, getLocalTimeZone, parseDate, today } from 'flat-internationalized-date'
import { useDateFormatter } from 'radix-vue'
import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'
import { Calendar } from '@/lib/registry/default/ui/calendar'
import { Button } from '@/lib/registry/default/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/default/ui/form'
import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/default/ui/popover'
import { toast } from '@/lib/registry/default/ui/toast'
import { cn } from '@/lib/utils'
const df = useDateFormatter('en')
const dateValue = ref<DateValue | undefined>()
const formSchema = toTypedSchema(z.object({
dob: z
.string()
.refine(v => v, { message: 'A date of birth is required.' }),
}))
const placeholder = ref()
const { handleSubmit } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
toast({
title: 'You submitted the following values:',
description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(values, null, 2))),
})
})
</script>
<template>
<form class="space-y-8" @submit="onSubmit">
<FormField v-slot="{ value }" name="dob">
<FormItem class="flex flex-col">
<FormLabel>Date of birth</FormLabel>
<Popover>
<PopoverTrigger as-child>
<FormControl>
<Button
variant="outline" :class="cn(
'w-[240px] ps-3 text-start font-normal',
!value && 'text-muted-foreground',
)"
>
<span>{{ value ? JSON.stringify(value) : "Pick a date" }} {{ JSON.stringify(value) }}</span>
<CalendarIcon class="ms-auto h-4 w-4 opacity-50" />
</Button>
<input hidden>
</FormControl>
</PopoverTrigger>
<PopoverContent class="p-0">
<Calendar
v-model:placeholder="placeholder"
v-model="dateValue"
calendar-label="Date of birth"
initial-focus
:min-value="createCalendarDate({
day: 1,
month: 2,
year: 2024,
})"
:max-value="today(getLocalTimeZone())"
@update:model-value="(v) => {
console.log(v)
}"
/>
</PopoverContent>
</Popover>
<FormDescription>
Your date of birth is used to calculate your age.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</Form>
</template>

View File

@ -0,0 +1,83 @@
<script setup lang="ts">
import { type HTMLAttributes, computed, onMounted, ref, toRef } from 'vue'
import { CalendarRoot, type CalendarRootEmits, type CalendarRootProps, useForwardPropsEmits } from 'radix-vue'
import { CALENDAR, DateFormatter, getLocalTimeZone, temporalToString, toCalendar, toDate, today } from 'flat-internationalized-date'
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading } from '@/lib/registry/default/ui/calendar'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/lib/registry/default/ui/select'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>(), {
modelValue: undefined,
placeholder() {
return toCalendar(today(getLocalTimeZone()), CALENDAR.GREGORIAN)
},
weekdayFormat: 'short',
})
const emits = defineEmits<CalendarRootEmits>()
const delegatedProps = computed(() => {
const { class: _, placeholder: __, ...delegated } = props
return delegated
})
const placeholder = toRef(props.placeholder)
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<CalendarRoot
v-slot="{ getMonths, getYears, formatter, date }"
v-model:placeholder="placeholder"
v-bind="forwarded"
:class="cn('rounded-md border p-3', props.class)"
>
{{ JSON.stringify(placeholder) }}
<CalendarHeader>
<CalendarHeading class="flex w-full items-center justify-between gap-2">
<Select
:default-value="props.placeholder.month.toString()"
>
<SelectTrigger aria-label="Select month" class="w-[60%]">
<SelectValue placeholder="Select month" />
</SelectTrigger>
<SelectContent class="max-h-[200px]">
<SelectItem
v-for="month in getMonths"
:key="temporalToString(month)" :value="month.month.toString()"
@click="placeholder = month"
>
{{ formatter.custom(month, { month: 'long' }) }}
</SelectItem>
</SelectContent>
</Select>
<Select
:default-value="props.placeholder.year.toString()"
>
<SelectTrigger aria-label="Select year" class="w-[40%]">
<SelectValue placeholder="Select year" />
</SelectTrigger>
<SelectContent class="max-h-[200px]">
<SelectItem
v-for="yearValue in getYears({ startIndex: -10, endIndex: 10 })"
:key="temporalToString(yearValue)" :value="yearValue.year.toString()"
@click="placeholder = yearValue"
>
{{ yearValue.year }}
</SelectItem>
</SelectContent>
</Select>
</CalendarHeading>
</CalendarHeader>
</CalendarRoot>
</template>

View File

@ -27,7 +27,7 @@ const frameworks = [
] ]
const open = ref(false) const open = ref(false)
const value = ref<string>('') const value = ref({})
// const filterFunction = (list: typeof frameworks, search: string) => list.filter(i => i.value.toLowerCase().includes(search.toLowerCase())) // const filterFunction = (list: typeof frameworks, search: string) => list.filter(i => i.value.toLowerCase().includes(search.toLowerCase()))
</script> </script>

View File

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

View File

@ -5,7 +5,7 @@ import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { ref } from 'vue' import { ref } from 'vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/default/ui/button' import { Button } from '@/lib/registry/default/ui/button'
import { Calendar } from '@/lib/registry/default/ui/calendar' import { Calendar } from '@/lib/registry/default/ui/v-calendar'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,

View File

@ -8,7 +8,7 @@ import * as z from 'zod'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/default/ui/button' import { Button } from '@/lib/registry/default/ui/button'
import { Calendar } from '@/lib/registry/default/ui/calendar' import { Calendar } from '@/lib/registry/default/ui/v-calendar'
import { import {
FormControl, FormControl,
FormDescription, FormDescription,

View File

@ -5,7 +5,7 @@ import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { ref } from 'vue' import { ref } from 'vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/default/ui/button' import { Button } from '@/lib/registry/default/ui/button'
import { Calendar } from '@/lib/registry/default/ui/calendar' import { Calendar } from '@/lib/registry/default/ui/v-calendar'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,

View File

@ -5,7 +5,7 @@ import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { ref } from 'vue' import { ref } from 'vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/default/ui/button' import { Button } from '@/lib/registry/default/ui/button'
import { Calendar } from '@/lib/registry/default/ui/calendar' import { Calendar } from '@/lib/registry/default/ui/v-calendar'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,

View File

@ -5,7 +5,7 @@ import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { ref } from 'vue' import { ref } from 'vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/default/ui/button' import { Button } from '@/lib/registry/default/ui/button'
import { Calendar } from '@/lib/registry/default/ui/calendar' import { Calendar } from '@/lib/registry/default/ui/v-calendar'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,

View File

@ -5,7 +5,7 @@ import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { ref } from 'vue' import { ref } from 'vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/default/ui/button' import { Button } from '@/lib/registry/default/ui/button'
import { Calendar } from '@/lib/registry/default/ui/calendar' import { Calendar } from '@/lib/registry/default/ui/v-calendar'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,

View File

@ -1,331 +1,60 @@
<script setup lang="ts"> <script lang="ts" setup>
import { useVModel } from '@vueuse/core' import { type HTMLAttributes, computed } from 'vue'
import { ChevronLeft, ChevronRight } from 'lucide-vue-next' import { CalendarRoot, type CalendarRootEmits, type CalendarRootProps, useForwardPropsEmits } from 'radix-vue'
import type { Calendar } from 'v-calendar' import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNextButton, CalendarPrevButton } from '.'
import { DatePicker } from 'v-calendar'
import { computed, nextTick, onMounted, ref, useSlots } from 'vue'
import { isVCalendarSlot } from '.'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { buttonVariants } from '@/lib/registry/default/ui/button'
/* Extracted from v-calendar */ const props = defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>()
type DatePickerModel = DatePickerDate | DatePickerRangeObject
type DateSource = Date | string | number
type DatePickerDate = DateSource | Partial<SimpleDateParts> | null
interface DatePickerRangeObject {
start: Exclude<DatePickerDate, null>
end: Exclude<DatePickerDate, null>
}
interface SimpleDateParts {
year: number
month: number
day: number
hours: number
minutes: number
seconds: number
milliseconds: number
}
defineOptions({ const emits = defineEmits<CalendarRootEmits>()
inheritAttrs: false,
})
const props = withDefaults(defineProps< {
modelValue?: string | number | Date | DatePickerModel
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, { const delegatedProps = computed(() => {
passive: true, const { class: _, ...delegated } = props
return delegated
}) })
const datePicker = ref<InstanceType<typeof DatePicker>>() const forwarded = useForwardPropsEmits(delegatedProps, emits)
// @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()
if (modelValue.value instanceof Date && calendarRef.value)
calendarRef.value.focusDate(modelValue.value)
})
const $slots = useSlots()
const vCalendarSlots = computed(() => {
return Object.keys($slots)
.filter(name => isVCalendarSlot(name))
.reduce((obj: Record<string, any>, key: string) => {
obj[key] = $slots[key]
return obj
}, {})
})
</script> </script>
<template> <template>
<div class="relative"> <CalendarRoot
<div v-if="$attrs.mode !== 'time'" class="absolute flex justify-between w-full px-4 top-3 z-[1]"> v-slot="{ grid, weekDays }"
<button :class="cn(buttonVariants({ variant: 'outline' }), 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100')" @click="handleNav('prev')"> :class="cn('p-3', props.class)"
<ChevronLeft class="w-4 h-4" /> v-bind="forwarded"
</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-bind="$attrs"
v-model="modelValue"
:model-modifiers="modelModifiers"
class="calendar"
trim-weeks
:transition="'none'"
:columns="columns"
> >
<template v-for="(_, slot) of vCalendarSlots" #[slot]="scope"> <CalendarHeader>
<slot :name="slot" v-bind="scope" /> <CalendarPrevButton />
</template> <CalendarHeading />
<CalendarNextButton />
</CalendarHeader>
<template #nav-prev-button> <div class="flex flex-col space-y-4 pt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
<ChevronLeft /> <CalendarGrid v-for="month in grid" :key="month.value.toString()">
</template> <CalendarGridHead>
<CalendarGridRow class="mb-1 grid w-full grid-cols-7">
<template #nav-next-button> <CalendarHeadCell
<ChevronRight /> v-for="day in weekDays" :key="day"
</template> >
</DatePicker> {{ day }}
</CalendarHeadCell>
</CalendarGridRow>
</CalendarGridHead>
<CalendarGridBody class="grid">
<CalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="grid grid-cols-7">
<CalendarCell
v-for="weekDate in weekDates"
:key="weekDate.toString()"
:date="weekDate"
>
<CalendarCellTrigger
:day="weekDate"
:month="month.value"
/>
</CalendarCell>
</CalendarGridRow>
</CalendarGridBody>
</CalendarGrid>
</div> </div>
</CalendarRoot>
</template> </template>
<style lang="css">
.calendar {
@apply p-3 text-center;
}
.calendar .vc-pane-layout {
@apply grid gap-4;
}
.calendar .vc-title {
@apply text-sm font-medium relative z-20;
}
.vc-popover-content-wrapper .vc-popover-content {
@apply mt-3 rounded-md max-w-xs border bg-background;
}
.vc-popover-content-wrapper .vc-nav-header {
@apply flex justify-between items-center p-2;
}
.vc-popover-content-wrapper .vc-nav-items {
@apply grid grid-cols-4 gap-2 p-2;
}
.vc-popover-content-wrapper .vc-nav-items .vc-nav-item {
@apply rounded-md px-2 py-1;
}
.vc-popover-content-wrapper .vc-nav-items .vc-nav-item:hover {
@apply text-muted-foreground bg-muted;
}
.vc-popover-content-wrapper .vc-nav-items .vc-nav-item.is-active {
@apply bg-primary text-primary-foreground;
}
.calendar .vc-pane-header-wrapper {
@apply hidden;
}
.calendar .vc-weeks {
@apply mt-4;
}
.calendar .vc-weekdays {
@apply justify-items-center;
}
.calendar .vc-weekday {
@apply text-muted-foreground rounded-md 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 first:rounded-l-md last:rounded-r-md;
}
.calendar .vc-day.is-today:not(:has(.vc-day-layer)) .vc-day-content {
@apply bg-secondary text-primary rounded-md;
}
.calendar .vc-day:has(.vc-highlight-base-start) {
@apply rounded-l-md;
}
.calendar .vc-day:has(.vc-highlight-base-end) {
@apply rounded-r-md;
}
.calendar .vc-day:has(.vc-highlight-bg-outline):not(:has(.vc-highlight-base-start)):not(:has(.vc-highlight-base-end)) {
@apply rounded-md;
}
.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 hover: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;
}
.calendar .vc-pane-container.in-transition {
@apply overflow-hidden;
}
.calendar .vc-pane-container {
@apply w-full relative;
}
:root {
--vc-slide-translate: 22px;
--vc-slide-duration: 0.15s;
--vc-slide-timing: ease;
}
.calendar .vc-fade-enter-active,
.calendar .vc-fade-leave-active,
.calendar .vc-slide-left-enter-active,
.calendar .vc-slide-left-leave-active,
.calendar .vc-slide-right-enter-active,
.calendar .vc-slide-right-leave-active,
.calendar .vc-slide-up-enter-active,
.calendar .vc-slide-up-leave-active,
.calendar .vc-slide-down-enter-active,
.calendar .vc-slide-down-leave-active,
.calendar .vc-slide-fade-enter-active,
.calendar .vc-slide-fade-leave-active {
transition:
opacity var(--vc-slide-duration) var(--vc-slide-timing),
-webkit-transform var(--vc-slide-duration) var(--vc-slide-timing);
transition:
transform var(--vc-slide-duration) var(--vc-slide-timing),
opacity var(--vc-slide-duration) var(--vc-slide-timing);
transition:
transform var(--vc-slide-duration) var(--vc-slide-timing),
opacity var(--vc-slide-duration) var(--vc-slide-timing),
-webkit-transform var(--vc-slide-duration) var(--vc-slide-timing);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
pointer-events: none;
}
.calendar .vc-none-leave-active,
.calendar .vc-fade-leave-active,
.calendar .vc-slide-left-leave-active,
.calendar .vc-slide-right-leave-active,
.calendar .vc-slide-up-leave-active,
.calendar .vc-slide-down-leave-active {
position: absolute !important;
width: 100%;
}
.calendar .vc-none-enter-from,
.calendar .vc-none-leave-to,
.calendar .vc-fade-enter-from,
.calendar .vc-fade-leave-to,
.calendar .vc-slide-left-enter-from,
.calendar .vc-slide-left-leave-to,
.calendar .vc-slide-right-enter-from,
.calendar .vc-slide-right-leave-to,
.calendar .vc-slide-up-enter-from,
.calendar .vc-slide-up-leave-to,
.calendar .vc-slide-down-enter-from,
.calendar .vc-slide-down-leave-to,
.calendar .vc-slide-fade-enter-from,
.calendar .vc-slide-fade-leave-to {
opacity: 0;
}
.calendar .vc-slide-left-enter-from,
.calendar .vc-slide-right-leave-to,
.calendar .vc-slide-fade-enter-from.direction-left,
.calendar .vc-slide-fade-leave-to.direction-left {
-webkit-transform: translateX(var(--vc-slide-translate));
transform: translateX(var(--vc-slide-translate));
}
.calendar .vc-slide-right-enter-from,
.calendar .vc-slide-left-leave-to,
.calendar .vc-slide-fade-enter-from.direction-right,
.calendar .vc-slide-fade-leave-to.direction-right {
-webkit-transform: translateX(calc(-1 * var(--vc-slide-translate)));
transform: translateX(calc(-1 * var(--vc-slide-translate)));
}
.calendar .vc-slide-up-enter-from,
.calendar .vc-slide-down-leave-to,
.calendar .vc-slide-fade-enter-from.direction-top,
.calendar .vc-slide-fade-leave-to.direction-top {
-webkit-transform: translateY(var(--vc-slide-translate));
transform: translateY(var(--vc-slide-translate));
}
.calendar .vc-slide-down-enter-from,
.calendar .vc-slide-up-leave-to,
.calendar .vc-slide-fade-enter-from.direction-bottom,
.calendar .vc-slide-fade-leave-to.direction-bottom {
-webkit-transform: translateY(calc(-1 * var(--vc-slide-translate)));
transform: translateY(calc(-1 * var(--vc-slide-translate)));
}
/**
* Timepicker styles
*/
.vc-time-picker {
@apply flex flex-col items-center p-2;
}
.vc-time-picker.vc-invalid {
@apply pointer-events-none opacity-50;
}
.vc-time-picker.vc-attached {
@apply border-t border-solid border-secondary mt-2;
}
.vc-time-picker > * + * {
@apply mt-1;
}
.vc-time-header {
@apply flex items-center text-sm font-semibold uppercase mt-1 px-1 leading-6;
}
.vc-time-select-group {
@apply inline-flex items-center px-1 rounded-md bg-primary-foreground border border-solid border-secondary;
}
.vc-time-select-group .vc-base-icon {
@apply mr-1 text-primary stroke-primary;
}
.vc-time-select-group select {
@apply bg-primary-foreground p-1 appearance-none outline-none text-center;
}
.vc-time-weekday {
@apply text-muted-foreground tracking-wide;
}
.vc-time-month {
@apply text-primary ml-2;
}
.vc-time-day {
@apply text-primary ml-1;
}
.vc-time-year {
@apply text-muted-foreground ml-2;
}
.vc-time-colon {
@apply mb-0.5;
}
.vc-time-decimal {
@apply ml-0.5;
}
</style>

View File

@ -0,0 +1,24 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarCell
:class="cn('relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50', props.class)"
v-bind="forwardedProps"
>
<slot />
</CalendarCell>
</template>

View File

@ -0,0 +1,38 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'radix-vue'
import { buttonVariants } from '@/lib/registry/default/ui/button'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarCellTrigger
:class="cn(
buttonVariants({ variant: 'ghost' }),
'h-9 w-9 p-0 font-normal',
'[&[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-month]:pointer-events-none data-[outside-month]:text-muted-foreground data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground [&[data-outside-month][data-selected]]:opacity-30',
props.class,
)"
v-bind="forwardedProps"
>
<slot />
</CalendarCellTrigger>
</template>

View File

@ -0,0 +1,24 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarGrid, type CalendarGridProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarGrid
:class="cn('w-full border-collapse space-y-1', props.class)"
v-bind="forwardedProps"
>
<slot />
</CalendarGrid>
</template>

View File

@ -0,0 +1,11 @@
<script lang="ts" setup>
import { CalendarGridBody, type CalendarGridBodyProps } from 'radix-vue'
const props = defineProps<CalendarGridBodyProps>()
</script>
<template>
<CalendarGridBody v-bind="props">
<slot />
</CalendarGridBody>
</template>

View File

@ -0,0 +1,12 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { CalendarGridHead, type CalendarGridHeadProps } from 'radix-vue'
const props = defineProps<CalendarGridHeadProps>()
</script>
<template>
<CalendarGridHead v-bind="props">
<slot />
</CalendarGridHead>
</template>

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarGridRow, type CalendarGridRowProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarGridRow :class="cn('flex', props.class)" v-bind="forwardedProps">
<slot />
</CalendarGridRow>
</template>

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarHeadCell, type CalendarHeadCellProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeadCell :class="cn('w-8 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)" v-bind="forwardedProps">
<slot />
</CalendarHeadCell>
</template>

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarHeader, type CalendarHeaderProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeader :class="cn('relative flex w-full items-center justify-between pt-1', props.class)" v-bind="forwardedProps">
<slot />
</CalendarHeader>
</template>

View File

@ -0,0 +1,27 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarHeading, type CalendarHeadingProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeadingProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeading
v-slot="{ headingValue }"
:class="cn('text-sm font-medium', props.class)"
v-bind="forwardedProps"
>
<slot :heading-value>
{{ headingValue }}
</slot>
</CalendarHeading>
</template>

View File

@ -0,0 +1,32 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarNext, type CalendarNextProps, useForwardProps } from 'radix-vue'
import { ChevronRight } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/lib/registry/default/ui/button'
const props = defineProps<CalendarNextProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarNext
:class="cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class,
)"
v-bind="forwardedProps"
>
<slot>
<ChevronRight class="h-4 w-4" />
</slot>
</CalendarNext>
</template>

View File

@ -0,0 +1,32 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarPrev, type CalendarPrevProps, useForwardProps } from 'radix-vue'
import { ChevronLeft } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/lib/registry/default/ui/button'
const props = defineProps<CalendarPrevProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarPrev
:class="cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class,
)"
v-bind="forwardedProps"
>
<slot>
<ChevronLeft class="h-4 w-4" />
</slot>
</CalendarPrev>
</template>

View File

@ -1,22 +1,12 @@
export { default as Calendar } from './Calendar.vue' export { default as Calendar } from './Calendar.vue'
import type { CalendarSlotName } from 'v-calendar/dist/types/src/components/Calendar/CalendarSlot.vue.d.ts' export { default as CalendarCell } from './CalendarCell.vue'
export { default as CalendarCellTrigger } from './CalendarCellTrigger.vue'
export function isVCalendarSlot(slotName: string): slotName is CalendarSlotName { export { default as CalendarGrid } from './CalendarGrid.vue'
const validSlots: CalendarSlotName[] = [ export { default as CalendarGridBody } from './CalendarGridBody.vue'
'day-content', export { default as CalendarGridHead } from './CalendarGridHead.vue'
'day-popover', export { default as CalendarGridRow } from './CalendarGridRow.vue'
'dp-footer', export { default as CalendarHeadCell } from './CalendarHeadCell.vue'
'footer', export { default as CalendarHeader } from './CalendarHeader.vue'
'header-title-wrapper', export { default as CalendarHeading } from './CalendarHeading.vue'
'header-title', export { default as CalendarNextButton } from './CalendarNextButton.vue'
'header-prev-button', export { default as CalendarPrevButton } from './CalendarPrevButton.vue'
'header-next-button',
'nav',
'nav-prev-button',
'nav-next-button',
'page',
'time-header',
]
return validSlots.includes(slotName as CalendarSlotName)
}

View File

@ -0,0 +1,331 @@
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
import type { Calendar } from 'v-calendar'
import { DatePicker } from 'v-calendar'
import { computed, nextTick, onMounted, ref, useSlots } from 'vue'
import { isVCalendarSlot } from '.'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/lib/registry/default/ui/button'
/* Extracted from v-calendar */
type DatePickerModel = DatePickerDate | DatePickerRangeObject
type DateSource = Date | string | number
type DatePickerDate = DateSource | Partial<SimpleDateParts> | null
interface DatePickerRangeObject {
start: Exclude<DatePickerDate, null>
end: Exclude<DatePickerDate, null>
}
interface SimpleDateParts {
year: number
month: number
day: number
hours: number
minutes: number
seconds: number
milliseconds: number
}
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps< {
modelValue?: string | number | Date | DatePickerModel
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()
if (modelValue.value instanceof Date && calendarRef.value)
calendarRef.value.focusDate(modelValue.value)
})
const $slots = useSlots()
const vCalendarSlots = computed(() => {
return Object.keys($slots)
.filter(name => isVCalendarSlot(name))
.reduce((obj: Record<string, any>, key: string) => {
obj[key] = $slots[key]
return obj
}, {})
})
</script>
<template>
<div class="relative">
<div v-if="$attrs.mode !== 'time'" class="absolute flex justify-between w-full px-4 top-3 z-[1]">
<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-bind="$attrs"
v-model="modelValue"
:model-modifiers="modelModifiers"
class="calendar"
trim-weeks
:transition="'none'"
:columns="columns"
>
<template v-for="(_, slot) of vCalendarSlots" #[slot]="scope">
<slot :name="slot" v-bind="scope" />
</template>
<template #nav-prev-button>
<ChevronLeft />
</template>
<template #nav-next-button>
<ChevronRight />
</template>
</DatePicker>
</div>
</template>
<style lang="css">
.calendar {
@apply p-3 text-center;
}
.calendar .vc-pane-layout {
@apply grid gap-4;
}
.calendar .vc-title {
@apply text-sm font-medium relative z-20;
}
.vc-popover-content-wrapper .vc-popover-content {
@apply mt-3 rounded-md max-w-xs border bg-background;
}
.vc-popover-content-wrapper .vc-nav-header {
@apply flex justify-between items-center p-2;
}
.vc-popover-content-wrapper .vc-nav-items {
@apply grid grid-cols-4 gap-2 p-2;
}
.vc-popover-content-wrapper .vc-nav-items .vc-nav-item {
@apply rounded-md px-2 py-1;
}
.vc-popover-content-wrapper .vc-nav-items .vc-nav-item:hover {
@apply text-muted-foreground bg-muted;
}
.vc-popover-content-wrapper .vc-nav-items .vc-nav-item.is-active {
@apply bg-primary text-primary-foreground;
}
.calendar .vc-pane-header-wrapper {
@apply hidden;
}
.calendar .vc-weeks {
@apply mt-4;
}
.calendar .vc-weekdays {
@apply justify-items-center;
}
.calendar .vc-weekday {
@apply text-muted-foreground rounded-md 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 first:rounded-l-md last:rounded-r-md;
}
.calendar .vc-day.is-today:not(:has(.vc-day-layer)) .vc-day-content {
@apply bg-secondary text-primary rounded-md;
}
.calendar .vc-day:has(.vc-highlight-base-start) {
@apply rounded-l-md;
}
.calendar .vc-day:has(.vc-highlight-base-end) {
@apply rounded-r-md;
}
.calendar .vc-day:has(.vc-highlight-bg-outline):not(:has(.vc-highlight-base-start)):not(:has(.vc-highlight-base-end)) {
@apply rounded-md;
}
.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 hover: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;
}
.calendar .vc-pane-container.in-transition {
@apply overflow-hidden;
}
.calendar .vc-pane-container {
@apply w-full relative;
}
:root {
--vc-slide-translate: 22px;
--vc-slide-duration: 0.15s;
--vc-slide-timing: ease;
}
.calendar .vc-fade-enter-active,
.calendar .vc-fade-leave-active,
.calendar .vc-slide-left-enter-active,
.calendar .vc-slide-left-leave-active,
.calendar .vc-slide-right-enter-active,
.calendar .vc-slide-right-leave-active,
.calendar .vc-slide-up-enter-active,
.calendar .vc-slide-up-leave-active,
.calendar .vc-slide-down-enter-active,
.calendar .vc-slide-down-leave-active,
.calendar .vc-slide-fade-enter-active,
.calendar .vc-slide-fade-leave-active {
transition:
opacity var(--vc-slide-duration) var(--vc-slide-timing),
-webkit-transform var(--vc-slide-duration) var(--vc-slide-timing);
transition:
transform var(--vc-slide-duration) var(--vc-slide-timing),
opacity var(--vc-slide-duration) var(--vc-slide-timing);
transition:
transform var(--vc-slide-duration) var(--vc-slide-timing),
opacity var(--vc-slide-duration) var(--vc-slide-timing),
-webkit-transform var(--vc-slide-duration) var(--vc-slide-timing);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
pointer-events: none;
}
.calendar .vc-none-leave-active,
.calendar .vc-fade-leave-active,
.calendar .vc-slide-left-leave-active,
.calendar .vc-slide-right-leave-active,
.calendar .vc-slide-up-leave-active,
.calendar .vc-slide-down-leave-active {
position: absolute !important;
width: 100%;
}
.calendar .vc-none-enter-from,
.calendar .vc-none-leave-to,
.calendar .vc-fade-enter-from,
.calendar .vc-fade-leave-to,
.calendar .vc-slide-left-enter-from,
.calendar .vc-slide-left-leave-to,
.calendar .vc-slide-right-enter-from,
.calendar .vc-slide-right-leave-to,
.calendar .vc-slide-up-enter-from,
.calendar .vc-slide-up-leave-to,
.calendar .vc-slide-down-enter-from,
.calendar .vc-slide-down-leave-to,
.calendar .vc-slide-fade-enter-from,
.calendar .vc-slide-fade-leave-to {
opacity: 0;
}
.calendar .vc-slide-left-enter-from,
.calendar .vc-slide-right-leave-to,
.calendar .vc-slide-fade-enter-from.direction-left,
.calendar .vc-slide-fade-leave-to.direction-left {
-webkit-transform: translateX(var(--vc-slide-translate));
transform: translateX(var(--vc-slide-translate));
}
.calendar .vc-slide-right-enter-from,
.calendar .vc-slide-left-leave-to,
.calendar .vc-slide-fade-enter-from.direction-right,
.calendar .vc-slide-fade-leave-to.direction-right {
-webkit-transform: translateX(calc(-1 * var(--vc-slide-translate)));
transform: translateX(calc(-1 * var(--vc-slide-translate)));
}
.calendar .vc-slide-up-enter-from,
.calendar .vc-slide-down-leave-to,
.calendar .vc-slide-fade-enter-from.direction-top,
.calendar .vc-slide-fade-leave-to.direction-top {
-webkit-transform: translateY(var(--vc-slide-translate));
transform: translateY(var(--vc-slide-translate));
}
.calendar .vc-slide-down-enter-from,
.calendar .vc-slide-up-leave-to,
.calendar .vc-slide-fade-enter-from.direction-bottom,
.calendar .vc-slide-fade-leave-to.direction-bottom {
-webkit-transform: translateY(calc(-1 * var(--vc-slide-translate)));
transform: translateY(calc(-1 * var(--vc-slide-translate)));
}
/**
* Timepicker styles
*/
.vc-time-picker {
@apply flex flex-col items-center p-2;
}
.vc-time-picker.vc-invalid {
@apply pointer-events-none opacity-50;
}
.vc-time-picker.vc-attached {
@apply border-t border-solid border-secondary mt-2;
}
.vc-time-picker > * + * {
@apply mt-1;
}
.vc-time-header {
@apply flex items-center text-sm font-semibold uppercase mt-1 px-1 leading-6;
}
.vc-time-select-group {
@apply inline-flex items-center px-1 rounded-md bg-primary-foreground border border-solid border-secondary;
}
.vc-time-select-group .vc-base-icon {
@apply mr-1 text-primary stroke-primary;
}
.vc-time-select-group select {
@apply bg-primary-foreground p-1 appearance-none outline-none text-center;
}
.vc-time-weekday {
@apply text-muted-foreground tracking-wide;
}
.vc-time-month {
@apply text-primary ml-2;
}
.vc-time-day {
@apply text-primary ml-1;
}
.vc-time-year {
@apply text-muted-foreground ml-2;
}
.vc-time-colon {
@apply mb-0.5;
}
.vc-time-decimal {
@apply ml-0.5;
}
</style>

View File

@ -0,0 +1,22 @@
export { default as Calendar } from './Calendar.vue'
import type { CalendarSlotName } from 'v-calendar/dist/types/src/components/Calendar/CalendarSlot.vue.d.ts'
export function isVCalendarSlot(slotName: string): slotName is CalendarSlotName {
const validSlots: CalendarSlotName[] = [
'day-content',
'day-popover',
'dp-footer',
'footer',
'header-title-wrapper',
'header-title',
'header-prev-button',
'header-next-button',
'nav',
'nav-prev-button',
'nav-next-button',
'page',
'time-header',
]
return validSlots.includes(slotName as CalendarSlotName)
}

View File

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

View File

@ -0,0 +1,80 @@
<script setup lang="ts">
import { h, ref } from 'vue'
import { DateFormatter, getLocalTimeZone, toDate, today } from 'flat-internationalized-date'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'
import { Calendar } from '@/lib/registry/default/ui/calendar'
import { Button } from '@/lib/registry/default/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/default/ui/form'
import { toast } from '@/lib/registry/default/ui/toast'
import { cn } from '@/lib/utils'
const df = DateFormatter('en-US')
const formSchema = toTypedSchema(z.object({
date: z.date({
required_error: 'A date of birth is required.',
}),
}))
const value = ref(today(getLocalTimeZone()))
const { handleSubmit } = useForm({
validationSchema: formSchema,
initialValues: {
date: toDate(value.value, getLocalTimeZone()),
},
})
const onSubmit = handleSubmit((values) => {
toast({
title: 'You submitted the following values:',
description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(values, null, 2))),
})
})
</script>
<template>
<Calendar v-model="value" :weekday-format="'short'" class="rounded-md border" />
<form class="space-y-8" @submit="onSubmit">
<FormField v-slot="{ componentField, value }" name="dob">
<FormItem class="flex flex-col">
<FormLabel>Date of birth</FormLabel>
<Popover>
<PopoverTrigger as-child>
<FormControl>
<Button
variant="outline" :class="cn(
'w-[240px] ps-3 text-start font-normal',
!value && 'text-muted-foreground',
)"
>
<span>{{ value ? df(value!) : "Pick a date" }}</span>
<CalendarIcon class="ms-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent class="p-0">
<Calendar v-bind="componentField" />
</PopoverContent>
</Popover>
<FormDescription>
Your date of birth is used to calculate your age.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</Form>
</template>

View File

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

View File

@ -5,7 +5,7 @@ import { CalendarIcon } from '@radix-icons/vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { Calendar } from '@/lib/registry/new-york/ui/calendar' import { Calendar } from '@/lib/registry/new-york/ui/v-calendar'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,

View File

@ -8,7 +8,7 @@ import * as z from 'zod'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { Calendar } from '@/lib/registry/new-york/ui/calendar' import { Calendar } from '@/lib/registry/new-york/ui/v-calendar'
import { import {
FormControl, FormControl,
FormDescription, FormDescription,

View File

@ -5,7 +5,7 @@ import { CalendarIcon } from '@radix-icons/vue'
import { ref } from 'vue' import { ref } from 'vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { Calendar } from '@/lib/registry/new-york/ui/calendar' import { Calendar } from '@/lib/registry/new-york/ui/v-calendar'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,

View File

@ -5,7 +5,7 @@ import { CalendarIcon } from '@radix-icons/vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { Calendar } from '@/lib/registry/new-york/ui/calendar' import { Calendar } from '@/lib/registry/new-york/ui/v-calendar'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,

View File

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { format } from 'date-fns' import { format } from 'date-fns'
import { ref } from 'vue'
import { CalendarIcon } from '@radix-icons/vue' import { CalendarIcon } from '@radix-icons/vue'
import { ref } from 'vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { Calendar } from '@/lib/registry/new-york/ui/calendar' import { Calendar } from '@/lib/registry/new-york/ui/v-calendar'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,

View File

@ -5,7 +5,7 @@ import { CalendarIcon } from '@radix-icons/vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { Calendar } from '@/lib/registry/new-york/ui/calendar' import { Calendar } from '@/lib/registry/new-york/ui/v-calendar'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,

View File

@ -1,325 +1,60 @@
<script setup lang="ts"> <script lang="ts" setup>
import { useVModel } from '@vueuse/core' import { type HTMLAttributes, computed } from 'vue'
import { ChevronLeftIcon, ChevronRightIcon } from '@radix-icons/vue' import { CalendarRoot, type CalendarRootEmits, type CalendarRootProps, useForwardPropsEmits } from 'radix-vue'
import type { Calendar } from 'v-calendar' import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNextButton, CalendarPrevButton } from '.'
import { DatePicker } from 'v-calendar'
import { computed, nextTick, onMounted, ref, useSlots } from 'vue'
import { isVCalendarSlot } from '.'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { buttonVariants } from '@/lib/registry/new-york/ui/button'
/* Extracted from v-calendar */ const props = defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>()
type DatePickerModel = DatePickerDate | DatePickerRangeObject
type DateSource = Date | string | number
type DatePickerDate = DateSource | Partial<SimpleDateParts> | null
interface DatePickerRangeObject {
start: Exclude<DatePickerDate, null>
end: Exclude<DatePickerDate, null>
}
interface SimpleDateParts {
year: number
month: number
day: number
hours: number
minutes: number
seconds: number
milliseconds: number
}
defineOptions({ const emits = defineEmits<CalendarRootEmits>()
inheritAttrs: false,
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
}) })
const props = withDefaults(defineProps<{ const forwarded = useForwardPropsEmits(delegatedProps, emits)
modelValue?: string | number | Date | DatePickerModel
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()
if (modelValue.value instanceof Date && calendarRef.value)
calendarRef.value.focusDate(modelValue.value)
})
const $slots = useSlots()
const vCalendarSlots = computed(() => {
return Object.keys($slots)
.filter(name => isVCalendarSlot(name))
.reduce((obj: Record<string, any>, key: string) => {
obj[key] = $slots[key]
return obj
}, {})
})
</script> </script>
<template> <template>
<div class="relative"> <CalendarRoot
<div v-if="$attrs.mode !== 'time'" class="absolute flex justify-between w-full px-4 top-3 z-[1]"> v-slot="{ grid, weekDays }"
<button :class="cn(buttonVariants({ variant: 'outline' }), 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100')" @click="handleNav('prev')"> :class="cn('p-3', props.class)"
<ChevronLeftIcon class="w-4 h-4" /> v-bind="forwarded"
</button>
<button :class="cn(buttonVariants({ variant: 'outline' }), 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100')" @click="handleNav('next')">
<ChevronRightIcon class="w-4 h-4" />
</button>
</div>
<DatePicker
ref="datePicker"
v-model="modelValue"
v-bind="$attrs"
:model-modifiers="modelModifiers"
class="calendar"
trim-weeks
:transition="'none'"
:columns="columns"
> >
<template v-for="(_, slot) of vCalendarSlots" #[slot]="scope"> <CalendarHeader>
<slot :name="slot" v-bind="scope" /> <CalendarPrevButton />
</template> <CalendarHeading />
<CalendarNextButton />
</CalendarHeader>
<template #nav-prev-button> <div class="flex flex-col space-y-4 pt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
<ChevronLeftIcon /> <CalendarGrid v-for="month in grid" :key="month.value.toString()">
</template> <CalendarGridHead>
<CalendarGridRow class="mb-1 grid w-full grid-cols-7">
<template #nav-next-button> <CalendarHeadCell
<ChevronRightIcon /> v-for="day in weekDays" :key="day"
</template> >
</DatePicker> {{ day }}
</CalendarHeadCell>
</CalendarGridRow>
</CalendarGridHead>
<CalendarGridBody class="grid">
<CalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="grid grid-cols-7">
<CalendarCell
v-for="weekDate in weekDates"
:key="weekDate.toString()"
:date="weekDate"
>
<CalendarCellTrigger
:day="weekDate"
:month="month.value"
/>
</CalendarCell>
</CalendarGridRow>
</CalendarGridBody>
</CalendarGrid>
</div> </div>
</CalendarRoot>
</template> </template>
<style lang="css">
.calendar {
@apply p-3 text-center;
}
.calendar .vc-pane-layout {
@apply grid gap-4;
}
.calendar .vc-title {
@apply text-sm font-medium relative z-20;
}
.vc-popover-content-wrapper .vc-popover-content {
@apply mt-3 rounded-md max-w-xs border bg-background;
}
.vc-popover-content-wrapper .vc-nav-header {
@apply flex justify-between items-center p-2;
}
.vc-popover-content-wrapper .vc-nav-items {
@apply grid grid-cols-4 gap-2 p-2;
}
.vc-popover-content-wrapper .vc-nav-items .vc-nav-item {
@apply rounded-md px-2 py-1;
}
.vc-popover-content-wrapper .vc-nav-items .vc-nav-item:hover {
@apply text-muted-foreground bg-muted;
}
.vc-popover-content-wrapper .vc-nav-items .vc-nav-item.is-active {
@apply bg-primary text-primary-foreground;
}
.calendar .vc-pane-header-wrapper {
@apply hidden;
}
.calendar .vc-weeks {
@apply mt-4;
}
.calendar .vc-weekdays {
@apply justify-items-center;
}
.calendar .vc-weekday {
@apply text-muted-foreground rounded-md 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 first:rounded-l-md last:rounded-r-md;
}
.calendar .vc-day.is-today:not(:has(.vc-day-layer)) .vc-day-content {
@apply bg-secondary text-primary rounded-md;
}
.calendar .vc-day:has(.vc-highlight-base-start) {
@apply rounded-l-md;
}
.calendar .vc-day:has(.vc-highlight-base-end) {
@apply rounded-r-md;
}
.calendar .vc-day:has(.vc-highlight-bg-outline):not(:has(.vc-highlight-base-start)):not(:has(.vc-highlight-base-end)) {
@apply rounded-md;
}
.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 hover: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;
}
.calendar .vc-pane-container.in-transition {
@apply overflow-hidden;
}
.calendar .vc-pane-container {
@apply w-full relative;
}
:root {
--vc-slide-translate: 22px;
--vc-slide-duration: 0.15s;
--vc-slide-timing: ease;
}
.calendar .vc-fade-enter-active,
.calendar .vc-fade-leave-active,
.calendar .vc-slide-left-enter-active,
.calendar .vc-slide-left-leave-active,
.calendar .vc-slide-right-enter-active,
.calendar .vc-slide-right-leave-active,
.calendar .vc-slide-up-enter-active,
.calendar .vc-slide-up-leave-active,
.calendar .vc-slide-down-enter-active,
.calendar .vc-slide-down-leave-active,
.calendar .vc-slide-fade-enter-active,
.calendar .vc-slide-fade-leave-active {
transition:
opacity var(--vc-slide-duration) var(--vc-slide-timing),
-webkit-transform var(--vc-slide-duration) var(--vc-slide-timing);
transition:
transform var(--vc-slide-duration) var(--vc-slide-timing),
opacity var(--vc-slide-duration) var(--vc-slide-timing);
transition:
transform var(--vc-slide-duration) var(--vc-slide-timing),
opacity var(--vc-slide-duration) var(--vc-slide-timing),
-webkit-transform var(--vc-slide-duration) var(--vc-slide-timing);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
pointer-events: none;
}
.calendar .vc-none-leave-active,
.calendar .vc-fade-leave-active,
.calendar .vc-slide-left-leave-active,
.calendar .vc-slide-right-leave-active,
.calendar .vc-slide-up-leave-active,
.calendar .vc-slide-down-leave-active {
position: absolute !important;
width: 100%;
}
.calendar .vc-none-enter-from,
.calendar .vc-none-leave-to,
.calendar .vc-fade-enter-from,
.calendar .vc-fade-leave-to,
.calendar .vc-slide-left-enter-from,
.calendar .vc-slide-left-leave-to,
.calendar .vc-slide-right-enter-from,
.calendar .vc-slide-right-leave-to,
.calendar .vc-slide-up-enter-from,
.calendar .vc-slide-up-leave-to,
.calendar .vc-slide-down-enter-from,
.calendar .vc-slide-down-leave-to,
.calendar .vc-slide-fade-enter-from,
.calendar .vc-slide-fade-leave-to {
opacity: 0;
}
.calendar .vc-slide-left-enter-from,
.calendar .vc-slide-right-leave-to,
.calendar .vc-slide-fade-enter-from.direction-left,
.calendar .vc-slide-fade-leave-to.direction-left {
-webkit-transform: translateX(var(--vc-slide-translate));
transform: translateX(var(--vc-slide-translate));
}
.calendar .vc-slide-right-enter-from,
.calendar .vc-slide-left-leave-to,
.calendar .vc-slide-fade-enter-from.direction-right,
.calendar .vc-slide-fade-leave-to.direction-right {
-webkit-transform: translateX(calc(-1 * var(--vc-slide-translate)));
transform: translateX(calc(-1 * var(--vc-slide-translate)));
}
.calendar .vc-slide-up-enter-from,
.calendar .vc-slide-down-leave-to,
.calendar .vc-slide-fade-enter-from.direction-top,
.calendar .vc-slide-fade-leave-to.direction-top {
-webkit-transform: translateY(var(--vc-slide-translate));
transform: translateY(var(--vc-slide-translate));
}
.calendar .vc-slide-down-enter-from,
.calendar .vc-slide-up-leave-to,
.calendar .vc-slide-fade-enter-from.direction-bottom,
.calendar .vc-slide-fade-leave-to.direction-bottom {
-webkit-transform: translateY(calc(-1 * var(--vc-slide-translate)));
transform: translateY(calc(-1 * var(--vc-slide-translate)));
}
/**
* Timepicker styles
*/
.vc-time-picker {
@apply flex flex-col items-center p-2;
}
.vc-time-picker.vc-invalid {
@apply pointer-events-none opacity-50;
}
.vc-time-picker.vc-attached {
@apply border-t border-solid border-secondary mt-2;
}
.vc-time-picker > * + * {
@apply mt-1;
}
.vc-time-header {
@apply flex items-center text-sm font-semibold uppercase mt-1 px-1 leading-6;
}
.vc-time-select-group {
@apply inline-flex items-center px-1 rounded-md bg-primary-foreground border border-solid border-secondary;
}
.vc-time-select-group .vc-base-icon {
@apply mr-1 text-primary stroke-primary;
}
.vc-time-select-group select {
@apply bg-primary-foreground p-1 appearance-none outline-none text-center;
}
.vc-time-weekday {
@apply text-muted-foreground tracking-wide;
}
.vc-time-month {
@apply text-primary ml-2;
}
.vc-time-day {
@apply text-primary ml-1;
}
.vc-time-year {
@apply text-muted-foreground ml-2;
}
.vc-time-colon {
@apply mb-0.5;
}
.vc-time-decimal {
@apply ml-0.5;
}
</style>

View File

@ -0,0 +1,24 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarCell
:class="cn('relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50', props.class)"
v-bind="forwardedProps"
>
<slot />
</CalendarCell>
</template>

View File

@ -0,0 +1,38 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'radix-vue'
import { buttonVariants } from '@/lib/registry/default/ui/button'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarCellTrigger
:class="cn(
buttonVariants({ variant: 'ghost' }),
'h-8 w-8 p-0 font-normal',
'[&[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-month]:pointer-events-none data-[outside-month]:text-muted-foreground data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground [&[data-outside-month][data-selected]]:opacity-30',
props.class,
)"
v-bind="forwardedProps"
>
<slot />
</CalendarCellTrigger>
</template>

View File

@ -0,0 +1,24 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarGrid, type CalendarGridProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarGrid
:class="cn('w-full border-collapse space-y-1', props.class)"
v-bind="forwardedProps"
>
<slot />
</CalendarGrid>
</template>

View File

@ -0,0 +1,11 @@
<script lang="ts" setup>
import { CalendarGridBody, type CalendarGridBodyProps } from 'radix-vue'
const props = defineProps<CalendarGridBodyProps>()
</script>
<template>
<CalendarGridBody v-bind="props">
<slot />
</CalendarGridBody>
</template>

View File

@ -0,0 +1,12 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { CalendarGridHead, type CalendarGridHeadProps } from 'radix-vue'
const props = defineProps<CalendarGridHeadProps & { class?: HTMLAttributes['class'] }>()
</script>
<template>
<CalendarGridHead v-bind="props">
<slot />
</CalendarGridHead>
</template>

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarGridRow, type CalendarGridRowProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarGridRow :class="cn('flex', props.class)" v-bind="forwardedProps">
<slot />
</CalendarGridRow>
</template>

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarHeadCell, type CalendarHeadCellProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeadCell :class="cn('w-8 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)" v-bind="forwardedProps">
<slot />
</CalendarHeadCell>
</template>

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarHeader, type CalendarHeaderProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeader :class="cn('relative flex w-full items-center justify-between pt-1', props.class)" v-bind="forwardedProps">
<slot />
</CalendarHeader>
</template>

View File

@ -0,0 +1,27 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarHeading, type CalendarHeadingProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeadingProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeading
v-slot="{ headingValue }"
:class="cn('text-sm font-medium', props.class)"
v-bind="forwardedProps"
>
<slot :heading-value>
{{ headingValue }}
</slot>
</CalendarHeading>
</template>

View File

@ -0,0 +1,32 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarNext, type CalendarNextProps, useForwardProps } from 'radix-vue'
import { ChevronRightIcon } from '@radix-icons/vue'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/lib/registry/default/ui/button'
const props = defineProps<CalendarNextProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarNext
:class="cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class,
)"
v-bind="forwardedProps"
>
<slot>
<ChevronRightIcon class="h-4 w-4" />
</slot>
</CalendarNext>
</template>

View File

@ -0,0 +1,32 @@
<script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarPrev, type CalendarPrevProps, useForwardProps } from 'radix-vue'
import { ChevronLeftIcon } from '@radix-icons/vue'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/lib/registry/default/ui/button'
const props = defineProps<CalendarPrevProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarPrev
:class="cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class,
)"
v-bind="forwardedProps"
>
<slot>
<ChevronLeftIcon class="h-4 w-4" />
</slot>
</CalendarPrev>
</template>

View File

@ -1,22 +1,12 @@
export { default as Calendar } from './Calendar.vue' export { default as Calendar } from './Calendar.vue'
import type { CalendarSlotName } from 'v-calendar/dist/types/src/components/Calendar/CalendarSlot.vue.d.ts' export { default as CalendarCell } from './CalendarCell.vue'
export { default as CalendarCellTrigger } from './CalendarCellTrigger.vue'
export function isVCalendarSlot(slotName: string): slotName is CalendarSlotName { export { default as CalendarGrid } from './CalendarGrid.vue'
const validSlots: CalendarSlotName[] = [ export { default as CalendarGridBody } from './CalendarGridBody.vue'
'day-content', export { default as CalendarGridHead } from './CalendarGridHead.vue'
'day-popover', export { default as CalendarGridRow } from './CalendarGridRow.vue'
'dp-footer', export { default as CalendarHeadCell } from './CalendarHeadCell.vue'
'footer', export { default as CalendarHeader } from './CalendarHeader.vue'
'header-title-wrapper', export { default as CalendarHeading } from './CalendarHeading.vue'
'header-title', export { default as CalendarNextButton } from './CalendarNextButton.vue'
'header-prev-button', export { default as CalendarPrevButton } from './CalendarPrevButton.vue'
'header-next-button',
'nav',
'nav-prev-button',
'nav-next-button',
'page',
'time-header',
]
return validSlots.includes(slotName as CalendarSlotName)
}

View File

@ -0,0 +1,325 @@
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { ChevronLeftIcon, ChevronRightIcon } from '@radix-icons/vue'
import type { Calendar } from 'v-calendar'
import { DatePicker } from 'v-calendar'
import { computed, nextTick, onMounted, ref, useSlots } from 'vue'
import { isVCalendarSlot } from '.'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/lib/registry/new-york/ui/button'
/* Extracted from v-calendar */
type DatePickerModel = DatePickerDate | DatePickerRangeObject
type DateSource = Date | string | number
type DatePickerDate = DateSource | Partial<SimpleDateParts> | null
interface DatePickerRangeObject {
start: Exclude<DatePickerDate, null>
end: Exclude<DatePickerDate, null>
}
interface SimpleDateParts {
year: number
month: number
day: number
hours: number
minutes: number
seconds: number
milliseconds: number
}
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<{
modelValue?: string | number | Date | DatePickerModel
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()
if (modelValue.value instanceof Date && calendarRef.value)
calendarRef.value.focusDate(modelValue.value)
})
const $slots = useSlots()
const vCalendarSlots = computed(() => {
return Object.keys($slots)
.filter(name => isVCalendarSlot(name))
.reduce((obj: Record<string, any>, key: string) => {
obj[key] = $slots[key]
return obj
}, {})
})
</script>
<template>
<div class="relative">
<div v-if="$attrs.mode !== 'time'" class="absolute flex justify-between w-full px-4 top-3 z-[1]">
<button :class="cn(buttonVariants({ variant: 'outline' }), 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100')" @click="handleNav('prev')">
<ChevronLeftIcon 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')">
<ChevronRightIcon class="w-4 h-4" />
</button>
</div>
<DatePicker
ref="datePicker"
v-model="modelValue"
v-bind="$attrs"
:model-modifiers="modelModifiers"
class="calendar"
trim-weeks
:transition="'none'"
:columns="columns"
>
<template v-for="(_, slot) of vCalendarSlots" #[slot]="scope">
<slot :name="slot" v-bind="scope" />
</template>
<template #nav-prev-button>
<ChevronLeftIcon />
</template>
<template #nav-next-button>
<ChevronRightIcon />
</template>
</DatePicker>
</div>
</template>
<style lang="css">
.calendar {
@apply p-3 text-center;
}
.calendar .vc-pane-layout {
@apply grid gap-4;
}
.calendar .vc-title {
@apply text-sm font-medium relative z-20;
}
.vc-popover-content-wrapper .vc-popover-content {
@apply mt-3 rounded-md max-w-xs border bg-background;
}
.vc-popover-content-wrapper .vc-nav-header {
@apply flex justify-between items-center p-2;
}
.vc-popover-content-wrapper .vc-nav-items {
@apply grid grid-cols-4 gap-2 p-2;
}
.vc-popover-content-wrapper .vc-nav-items .vc-nav-item {
@apply rounded-md px-2 py-1;
}
.vc-popover-content-wrapper .vc-nav-items .vc-nav-item:hover {
@apply text-muted-foreground bg-muted;
}
.vc-popover-content-wrapper .vc-nav-items .vc-nav-item.is-active {
@apply bg-primary text-primary-foreground;
}
.calendar .vc-pane-header-wrapper {
@apply hidden;
}
.calendar .vc-weeks {
@apply mt-4;
}
.calendar .vc-weekdays {
@apply justify-items-center;
}
.calendar .vc-weekday {
@apply text-muted-foreground rounded-md 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 first:rounded-l-md last:rounded-r-md;
}
.calendar .vc-day.is-today:not(:has(.vc-day-layer)) .vc-day-content {
@apply bg-secondary text-primary rounded-md;
}
.calendar .vc-day:has(.vc-highlight-base-start) {
@apply rounded-l-md;
}
.calendar .vc-day:has(.vc-highlight-base-end) {
@apply rounded-r-md;
}
.calendar .vc-day:has(.vc-highlight-bg-outline):not(:has(.vc-highlight-base-start)):not(:has(.vc-highlight-base-end)) {
@apply rounded-md;
}
.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 hover: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;
}
.calendar .vc-pane-container.in-transition {
@apply overflow-hidden;
}
.calendar .vc-pane-container {
@apply w-full relative;
}
:root {
--vc-slide-translate: 22px;
--vc-slide-duration: 0.15s;
--vc-slide-timing: ease;
}
.calendar .vc-fade-enter-active,
.calendar .vc-fade-leave-active,
.calendar .vc-slide-left-enter-active,
.calendar .vc-slide-left-leave-active,
.calendar .vc-slide-right-enter-active,
.calendar .vc-slide-right-leave-active,
.calendar .vc-slide-up-enter-active,
.calendar .vc-slide-up-leave-active,
.calendar .vc-slide-down-enter-active,
.calendar .vc-slide-down-leave-active,
.calendar .vc-slide-fade-enter-active,
.calendar .vc-slide-fade-leave-active {
transition:
opacity var(--vc-slide-duration) var(--vc-slide-timing),
-webkit-transform var(--vc-slide-duration) var(--vc-slide-timing);
transition:
transform var(--vc-slide-duration) var(--vc-slide-timing),
opacity var(--vc-slide-duration) var(--vc-slide-timing);
transition:
transform var(--vc-slide-duration) var(--vc-slide-timing),
opacity var(--vc-slide-duration) var(--vc-slide-timing),
-webkit-transform var(--vc-slide-duration) var(--vc-slide-timing);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
pointer-events: none;
}
.calendar .vc-none-leave-active,
.calendar .vc-fade-leave-active,
.calendar .vc-slide-left-leave-active,
.calendar .vc-slide-right-leave-active,
.calendar .vc-slide-up-leave-active,
.calendar .vc-slide-down-leave-active {
position: absolute !important;
width: 100%;
}
.calendar .vc-none-enter-from,
.calendar .vc-none-leave-to,
.calendar .vc-fade-enter-from,
.calendar .vc-fade-leave-to,
.calendar .vc-slide-left-enter-from,
.calendar .vc-slide-left-leave-to,
.calendar .vc-slide-right-enter-from,
.calendar .vc-slide-right-leave-to,
.calendar .vc-slide-up-enter-from,
.calendar .vc-slide-up-leave-to,
.calendar .vc-slide-down-enter-from,
.calendar .vc-slide-down-leave-to,
.calendar .vc-slide-fade-enter-from,
.calendar .vc-slide-fade-leave-to {
opacity: 0;
}
.calendar .vc-slide-left-enter-from,
.calendar .vc-slide-right-leave-to,
.calendar .vc-slide-fade-enter-from.direction-left,
.calendar .vc-slide-fade-leave-to.direction-left {
-webkit-transform: translateX(var(--vc-slide-translate));
transform: translateX(var(--vc-slide-translate));
}
.calendar .vc-slide-right-enter-from,
.calendar .vc-slide-left-leave-to,
.calendar .vc-slide-fade-enter-from.direction-right,
.calendar .vc-slide-fade-leave-to.direction-right {
-webkit-transform: translateX(calc(-1 * var(--vc-slide-translate)));
transform: translateX(calc(-1 * var(--vc-slide-translate)));
}
.calendar .vc-slide-up-enter-from,
.calendar .vc-slide-down-leave-to,
.calendar .vc-slide-fade-enter-from.direction-top,
.calendar .vc-slide-fade-leave-to.direction-top {
-webkit-transform: translateY(var(--vc-slide-translate));
transform: translateY(var(--vc-slide-translate));
}
.calendar .vc-slide-down-enter-from,
.calendar .vc-slide-up-leave-to,
.calendar .vc-slide-fade-enter-from.direction-bottom,
.calendar .vc-slide-fade-leave-to.direction-bottom {
-webkit-transform: translateY(calc(-1 * var(--vc-slide-translate)));
transform: translateY(calc(-1 * var(--vc-slide-translate)));
}
/**
* Timepicker styles
*/
.vc-time-picker {
@apply flex flex-col items-center p-2;
}
.vc-time-picker.vc-invalid {
@apply pointer-events-none opacity-50;
}
.vc-time-picker.vc-attached {
@apply border-t border-solid border-secondary mt-2;
}
.vc-time-picker > * + * {
@apply mt-1;
}
.vc-time-header {
@apply flex items-center text-sm font-semibold uppercase mt-1 px-1 leading-6;
}
.vc-time-select-group {
@apply inline-flex items-center px-1 rounded-md bg-primary-foreground border border-solid border-secondary;
}
.vc-time-select-group .vc-base-icon {
@apply mr-1 text-primary stroke-primary;
}
.vc-time-select-group select {
@apply bg-primary-foreground p-1 appearance-none outline-none text-center;
}
.vc-time-weekday {
@apply text-muted-foreground tracking-wide;
}
.vc-time-month {
@apply text-primary ml-2;
}
.vc-time-day {
@apply text-primary ml-1;
}
.vc-time-year {
@apply text-muted-foreground ml-2;
}
.vc-time-colon {
@apply mb-0.5;
}
.vc-time-decimal {
@apply ml-0.5;
}
</style>

View File

@ -0,0 +1,22 @@
export { default as Calendar } from './Calendar.vue'
import type { CalendarSlotName } from 'v-calendar/dist/types/src/components/Calendar/CalendarSlot.vue.d.ts'
export function isVCalendarSlot(slotName: string): slotName is CalendarSlotName {
const validSlots: CalendarSlotName[] = [
'day-content',
'day-popover',
'dp-footer',
'footer',
'header-title-wrapper',
'header-title',
'header-prev-button',
'header-next-button',
'nav',
'nav-prev-button',
'nav-next-button',
'page',
'time-header',
]
return validSlots.includes(slotName as CalendarSlotName)
}

View File

@ -117,16 +117,24 @@
}, },
{ {
"name": "calendar", "name": "calendar",
"dependencies": [ "dependencies": [],
"@vueuse/core",
"v-calendar@next"
],
"registryDependencies": [ "registryDependencies": [
"utils", "utils",
"button" "button"
], ],
"files": [ "files": [
"ui/calendar/Calendar.vue", "ui/calendar/Calendar.vue",
"ui/calendar/CalendarCell.vue",
"ui/calendar/CalendarCellTrigger.vue",
"ui/calendar/CalendarGrid.vue",
"ui/calendar/CalendarGridBody.vue",
"ui/calendar/CalendarGridHead.vue",
"ui/calendar/CalendarGridRow.vue",
"ui/calendar/CalendarHeadCell.vue",
"ui/calendar/CalendarHeader.vue",
"ui/calendar/CalendarHeading.vue",
"ui/calendar/CalendarNextButton.vue",
"ui/calendar/CalendarPrevButton.vue",
"ui/calendar/index.ts" "ui/calendar/index.ts"
], ],
"type": "components:ui" "type": "components:ui"
@ -735,5 +743,21 @@
"ui/tooltip/index.ts" "ui/tooltip/index.ts"
], ],
"type": "components:ui" "type": "components:ui"
},
{
"name": "v-calendar",
"dependencies": [
"@vueuse/core",
"v-calendar@next"
],
"registryDependencies": [
"utils",
"button"
],
"files": [
"ui/v-calendar/Calendar.vue",
"ui/v-calendar/index.ts"
],
"type": "components:ui"
} }
] ]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,11 @@ settings:
autoInstallPeers: true autoInstallPeers: true
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
patchedDependencies:
radix-vue@1.5.3:
hash: jnhemw3hcquqa2vjldfytqhpcy
path: patches/radix-vue@1.5.3.patch
importers: importers:
.: .:
@ -86,6 +91,9 @@ importers:
embla-carousel-vue: embla-carousel-vue:
specifier: ^8.0.0 specifier: ^8.0.0
version: 8.0.0(vue@3.4.21) version: 8.0.0(vue@3.4.21)
flat-internationalized-date:
specifier: ^1.2.3
version: 1.2.3
lucide-vue-next: lucide-vue-next:
specifier: ^0.359.0 specifier: ^0.359.0
version: 0.359.0(vue@3.4.21) version: 0.359.0(vue@3.4.21)
@ -94,7 +102,7 @@ importers:
version: 0.30.8 version: 0.30.8
radix-vue: radix-vue:
specifier: ^1.5.3 specifier: ^1.5.3
version: 1.5.3(vue@3.4.21) version: 1.5.3(patch_hash=jnhemw3hcquqa2vjldfytqhpcy)(vue@3.4.21)
tailwindcss-animate: tailwindcss-animate:
specifier: ^1.0.7 specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.1) version: 1.0.7(tailwindcss@3.4.1)
@ -257,7 +265,7 @@ importers:
version: 2.4.2 version: 2.4.2
radix-vue: radix-vue:
specifier: ^1.5.3 specifier: ^1.5.3
version: 1.5.3(vue@3.4.21) version: 1.5.3(patch_hash=jnhemw3hcquqa2vjldfytqhpcy)(vue@3.4.21)
ts-morph: ts-morph:
specifier: ^22.0.0 specifier: ^22.0.0
version: 22.0.0 version: 22.0.0
@ -5386,7 +5394,7 @@ packages:
'@babel/helper-module-imports': 7.22.15 '@babel/helper-module-imports': 7.22.15
'@babel/helper-plugin-utils': 7.22.5 '@babel/helper-plugin-utils': 7.22.5
'@babel/parser': 7.24.1 '@babel/parser': 7.24.1
'@vue/compiler-sfc': 3.4.21 '@vue/compiler-sfc': 3.4.19
dev: true dev: true
/@vue/compiler-core@3.4.19: /@vue/compiler-core@3.4.19:
@ -8770,6 +8778,12 @@ packages:
rimraf: 3.0.2 rimraf: 3.0.2
dev: true dev: true
/flat-internationalized-date@1.2.3:
resolution: {integrity: sha512-qhNeqjiPSzfbaDXa+2HPVb+U2xNQKHpT9gAn8TJi1Hc2CvmgudR78oltH1YQLe91ffgk2paNcHZtX8YLEh7KMA==}
dependencies:
typescript: 5.4.2
dev: false
/flat@5.0.2: /flat@5.0.2:
resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
hasBin: true hasBin: true
@ -12489,7 +12503,7 @@ packages:
resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==}
dev: false dev: false
/radix-vue@1.5.3(vue@3.4.21): /radix-vue@1.5.3(patch_hash=jnhemw3hcquqa2vjldfytqhpcy)(vue@3.4.21):
resolution: {integrity: sha512-K1JF8P238jGKRwwlWe0LNCd80bamfWFnDhLNBAgoWvSRORRIsoo7DODnC4TLE62JE55tf/6ABSs5JIvp2BvYPA==} resolution: {integrity: sha512-K1JF8P238jGKRwwlWe0LNCd80bamfWFnDhLNBAgoWvSRORRIsoo7DODnC4TLE62JE55tf/6ABSs5JIvp2BvYPA==}
dependencies: dependencies:
'@floating-ui/dom': 1.6.1 '@floating-ui/dom': 1.6.1
@ -12501,6 +12515,7 @@ packages:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
dev: false dev: false
patched: true
/radix3@1.1.1: /radix3@1.1.1:
resolution: {integrity: sha512-yUUd5VTiFtcMEx0qFUxGAv5gbMc1un4RvEO1JZdP7ZUl/RHygZK6PknIKntmQRZxnMY3ZXD2ISaw1ij8GYW1yg==} resolution: {integrity: sha512-yUUd5VTiFtcMEx0qFUxGAv5gbMc1un4RvEO1JZdP7ZUl/RHygZK6PknIKntmQRZxnMY3ZXD2ISaw1ij8GYW1yg==}
@ -14500,7 +14515,7 @@ packages:
resolution: {integrity: sha512-3PYWMbN3cSdsciv3fzewskxZFnX61PYq1uNsbvizXDo/8sN4SMrWkYDqWaPdTD3GTEm6wpx7j5flRLg7A5ZXbQ==} resolution: {integrity: sha512-3PYWMbN3cSdsciv3fzewskxZFnX61PYq1uNsbvizXDo/8sN4SMrWkYDqWaPdTD3GTEm6wpx7j5flRLg7A5ZXbQ==}
dependencies: dependencies:
'@vueuse/core': 10.9.0(vue@3.4.21) '@vueuse/core': 10.9.0(vue@3.4.21)
radix-vue: 1.5.3(vue@3.4.21) radix-vue: 1.5.3(patch_hash=jnhemw3hcquqa2vjldfytqhpcy)(vue@3.4.21)
vue: 3.4.21(typescript@5.4.2) vue: 3.4.21(typescript@5.4.2)
transitivePeerDependencies: transitivePeerDependencies:
- '@vue/composition-api' - '@vue/composition-api'