Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Calendar): implement component #2618

Open
wants to merge 34 commits into
base: v3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
60ddf41
feat(calendar): implement component
hywax Nov 12, 2024
8edd8f2
feat(Calendar): base struct
hywax Nov 13, 2024
fc9d69b
feat(Calendar): style
hywax Nov 13, 2024
e2e78b1
Merge branch 'v3' into radix-calendar
hywax Nov 13, 2024
0ebafb5
feat(Calendar): multi
hywax Nov 13, 2024
62e3f63
feat(Calendar): fix color
hywax Nov 13, 2024
975df20
feat(Calendar): natural headCell
hywax Nov 13, 2024
5a5680d
feat(Calendar): playground
hywax Nov 13, 2024
e0f40fa
feat(Calendar): update
hywax Nov 13, 2024
2a12716
feat(Calendar): update
hywax Nov 13, 2024
d54d5e2
Merge branch 'v3' into radix-calendar
hywax Nov 14, 2024
20532bf
feat(Calendar): component
hywax Nov 14, 2024
f7e58c9
feat(Calendar): style
hywax Nov 15, 2024
01c967c
Merge branch 'v3' into radix-calendar
hywax Nov 15, 2024
66f8078
feat(Calendar): to native `Date`
hywax Nov 15, 2024
4b35059
feat(Calendar): prepare docs
hywax Nov 15, 2024
cd0ba13
feat(Calendar): range
hywax Nov 15, 2024
b80efc2
feat(Calendar): minValue, maxValue
hywax Nov 15, 2024
89b8bde
feat(Calendar): add issue
hywax Nov 15, 2024
2776405
feat(Calendar): defaultValue
hywax Nov 15, 2024
99cdb83
feat(Calendar): date
hywax Nov 15, 2024
14ec31e
feat(Calendar): model
hywax Nov 15, 2024
b3ab240
feat(Calendar): tests
hywax Nov 15, 2024
330bc8d
feat(Calendar): locale
hywax Nov 17, 2024
780641a
feat(ColorPicker): fix
hywax Nov 18, 2024
84ef6e2
feat(ColorPicker): fix 2
hywax Nov 18, 2024
22913dd
feat(ColorPicker): fix 3
hywax Nov 18, 2024
5fa1cbe
feat(ColorPicker): fix 4
hywax Nov 18, 2024
c0f4680
feat(ColorPicker): fix 5
hywax Nov 18, 2024
479b369
Merge branch 'v3' into radix-calendar
hywax Nov 18, 2024
42c9e7a
feat(ColorPicker): fix 6
hywax Nov 18, 2024
2af16b5
feat(ColorPicker): fix 7
hywax Nov 18, 2024
af5ef66
feat(ColorPicker): fix 8
hywax Nov 18, 2024
71541ec
feat(ColorPicker): docs
hywax Nov 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/content/3.components/calendar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
description:
links:
- label: Calendar
icon: i-custom-radix-vue
to: https://www.radix-vue.com/components/calendar.html
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Calendar.vue
---

## Usage

## Examples

## API

### Props

:component-props

### Slots

:component-slots

### Emits

:component-emits

## Theme

:component-theme
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
},
"dependencies": {
"@iconify/vue": "^4.1.2",
"@internationalized/date": "^3.5.6",
"@nuxt/devtools-kit": "^1.6.0",
"@nuxt/fonts": "^0.10.2",
"@nuxt/icon": "^1.7.2",
Expand Down
1 change: 1 addition & 0 deletions playground/app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const components = [
'button',
'button-group',
'card',
'calendar',
'carousel',
'checkbox',
'chip',
Expand Down
46 changes: 46 additions & 0 deletions playground/app/pages/components/calendar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { startOfWeek, endOfWeek, now, getLocalTimeZone } from '@internationalized/date'
import theme from '#build/ui/calendar'

const colors = Object.keys(theme.variants.color) as Array<keyof typeof theme.variants.color>
const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.variants.size>

const singleValue = ref(new Date())
const multipleValue = ref([
startOfWeek(now(getLocalTimeZone()), 'en').toDate(),
endOfWeek(now(getLocalTimeZone()), 'en').toDate()
])
</script>

<template>
<div class="flex flex-col gap-4">
<div class="flex justify-center gap-2">
<UCalendar v-model="singleValue" />
</div>
<div class="flex items-center gap-4">
<UCalendar
v-for="color in colors"
:key="color"
v-model="singleValue"
:color="color"
/>
</div>
<div class="flex items-center gap-4">
<UCalendar
v-for="color in colors"
:key="color"
v-model="multipleValue"
type="multiple"
:color="color"
/>
</div>
<div class="flex items-center gap-4">
<UCalendar
v-for="size in sizes"
:key="size"
v-model="singleValue"
:size="size"
/>
</div>
</div>
</template>
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

145 changes: 145 additions & 0 deletions src/runtime/components/Calendar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<script lang="ts">
import { computed } from 'vue'
import { tv, type VariantProps } from 'tailwind-variants'
import type { CalendarRootProps, CalendarRootEmits, CalendarCellTriggerProps } from 'radix-vue'
import type { DateValue } from '@internationalized/date'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/calendar'

const appConfig = _appConfig as AppConfig & { ui: { calendar: Partial<typeof theme> } }

const calendar = tv({ extend: tv(theme), ...(appConfig.ui?.calendar || {}) })

type CalendarVariants = VariantProps<typeof calendar>

interface BaseCalendarProps extends Pick<CalendarRootProps, 'weekStartsOn' | 'weekdayFormat' | 'fixedWeeks' | 'maxValue' | 'minValue' | 'numberOfMonths' | 'disabled' | 'readonly' | 'initialFocus' | 'isDateDisabled' | 'isDateUnavailable' | 'dir'> {
color?: CalendarVariants['color']
size?: CalendarVariants['size']
class?: any
ui?: Partial<typeof calendar.slots>
/**
* The locale to use for formatting and parsing numbers.
* @defaultValue UApp.locale.code
*/
locale?: string
type?: 'multiple' | 'single'
}

export interface CalendarMultipleProps extends BaseCalendarProps {
modelValue?: [DateValue, DateValue]
defaultValue?: [DateValue, DateValue]
type?: 'multiple'
}

export interface CalendarSingleProps extends BaseCalendarProps {
modelValue?: DateValue
defaultValue?: DateValue
type?: 'single'
}

export type CalendarProps = CalendarMultipleProps | CalendarSingleProps

export interface CalendarEmits extends CalendarRootEmits {}

export interface CalendarSlots {
'heading': (props: { value: string }) => any
'day': (props: Pick<CalendarCellTriggerProps, 'day'>) => any
'week-day': (props: { day: string }) => any
}
</script>

<script setup lang="ts">
import { useForwardPropsEmits, CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNext, CalendarPrev, CalendarRoot } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import UButton from './Button.vue'
import { useLocale } from '../composables/useLocale'
import type { Direction } from '../types'

const props = withDefaults(defineProps<CalendarProps>(), {
fixedWeeks: true,
type: 'single'
})
const emits = defineEmits<CalendarEmits>()
defineSlots<CalendarSlots>()

const { code: codeLocale, dir: dirLocale } = useLocale()
const locale = computed(() => props.locale || codeLocale.value)
const dir = computed(() => (props.dir || dirLocale.value) as Direction)

const _rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'modelValue'), emits)

const ui = computed(() => calendar({
color: props.color,
size: props.size
}))
</script>

<template>
<CalendarRoot
v-slot="{ weekDays, grid }"
:class="ui.root({ class: [props.class, props.ui?.root] })"
:locale="locale"
:dir="dir"
fixed-weeks
>
<CalendarHeader :class="ui.header({ class: props.ui?.header })">
<CalendarPrev as-child>
<UButton :icon="appConfig.ui.icons.chevronLeft" :size="props.size" color="neutral" variant="ghost" />
</CalendarPrev>
<CalendarHeading v-slot="{ headingValue }" :class="ui.heading({ class: props.ui?.heading })">
<slot name="heading" :value="headingValue">
{{ headingValue }}
</slot>
</CalendarHeading>
<CalendarNext as-child>
<UButton :icon="appConfig.ui.icons.chevronRight" :size="props.size" color="neutral" variant="ghost" />
</CalendarNext>
</CalendarHeader>
<div :class="ui.body({ class: props.ui?.body })">
<CalendarGrid
v-for="month in grid"
:key="month.value.toString()"
:class="ui.grid({ class: props.ui?.grid })"
>
<CalendarGridHead>
<CalendarGridRow :class="ui.gridWeekDaysRow({ class: props.ui?.gridWeekDaysRow })">
<CalendarHeadCell
v-for="day in weekDays"
:key="day"
:class="ui.headCell({ class: props.ui?.headCell })"
>
<slot name="week-day" :day="day">
{{ day }}
</slot>
</CalendarHeadCell>
</CalendarGridRow>
</CalendarGridHead>
<CalendarGridBody :class="ui.gridBody({ class: props.ui?.gridBody })">
<CalendarGridRow
v-for="(weekDates, index) in month.rows"
:key="`weekDate-${index}`"
:class="ui.gridRow({ class: props.ui?.gridRow })"
>
<CalendarCell
v-for="weekDate in weekDates"
:key="weekDate.toString()"
:date="weekDate"
:class="ui.cell({ class: props.ui?.cell })"
>
<CalendarCellTrigger
:day="weekDate"
:month="month.value"
:class="ui.cellTrigger({ class: props.ui?.cellTrigger })"
>
<slot name="day" :day="weekDate">
{{ weekDate.day }}
</slot>
</CalendarCellTrigger>
</CalendarCell>
</CalendarGridRow>
</CalendarGridBody>
</CalendarGrid>
</div>
</CalendarRoot>
</template>
1 change: 1 addition & 0 deletions src/runtime/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from '../components/AvatarGroup.vue'
export * from '../components/Badge.vue'
export * from '../components/Breadcrumb.vue'
export * from '../components/Button.vue'
export * from '../components/Calendar.vue'
export * from '../components/Card.vue'
export * from '../components/Carousel.vue'
export * from '../components/Checkbox.vue'
Expand Down
66 changes: 66 additions & 0 deletions src/theme/calendar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { ModuleOptions } from '../module'

export default (options: Required<ModuleOptions>) => {
return {
slots: {
root: 'rounded-[calc(var(--ui-radius)*1.5)]',
header: 'flex items-center justify-between',
body: 'flex flex-col space-y-4 pt-4 sm:flex-row sm:space-x-4 sm:space-y-0',
heading: 'text-sm font-medium',
grid: 'w-full border-collapse select-none space-y-1',
gridRow: 'grid grid-cols-7',
gridWeekDaysRow: 'mb-1 grid w-full grid-cols-7',
gridBody: 'grid',
headCell: 'rounded-md text-xs',
cell: 'relative text-center text-sm',
cellTrigger: ['relative flex items-center justify-center rounded-full whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-inset data-[disabled]:text-[var(--ui-text)]/30 data-[selected]:text-[var(--ui-bg)] hover:bg-[var(--ui-bg-elevated)]', options.theme.transitions && 'transition-[color,opacity] duration-200']
},
variants: {
color: {
...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, {
headCell: `text-[var(--ui-${color})]`,
cellTrigger: `focus-visible:ring-[var(--ui-${color})] data-[selected]:!bg-[var(--ui-${color})] data-[today]:bg-[var(--ui-${color})]/10`
hywax marked this conversation as resolved.
Show resolved Hide resolved
}])),
neutral: {
cellTrigger: ''
}
},
size: {
xs: {
heading: 'text-xs',
cell: 'text-xs',
headCell: 'text-[10px]',
cellTrigger: 'w-7 h-7',
body: 'space-y-2 pt-2'
},
sm: {
heading: 'text-xs',
cell: 'text-xs',
cellTrigger: 'w-7 h-7'
},
md: {
cellTrigger: 'w-8 h-8'
},
lg: {
heading: 'text-md',
headCell: 'text-md',
cellTrigger: 'w-9 h-9 text-md'
},
xl: {
heading: 'text-lg',
headCell: 'text-lg',
cellTrigger: 'w-10 h-10 text-lg'
}
}
},
compoundVariants: [
...(options.theme.colors || []).map((color: string) => ({
color
}))
],
defaultVariants: {
size: 'md',
color: 'primary'
}
}
}
1 change: 1 addition & 0 deletions src/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { default as badge } from './badge'
export { default as breadcrumb } from './breadcrumb'
export { default as button } from './button'
export { default as buttonGroup } from './button-group'
export { default as calendar } from './calendar'
export { default as card } from './card'
export { default as carousel } from './carousel'
export { default as checkbox } from './checkbox'
Expand Down
17 changes: 17 additions & 0 deletions test/components/Calendar.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, it, expect } from 'vitest'
import Calendar, { type CalendarProps, type CalendarSlots } from '../../src/runtime/components/Calendar.vue'
import ComponentRender from '../component-render'

describe('Calendar', () => {
it.each([
// Props
['with as', { props: { as: 'div' } }],
['with class', { props: { class: '' } }],
['with ui', { props: { ui: {} } }],
// Slots
['with default slot', { slots: { default: () => 'Default slot' } }]
])('renders %s correctly', async (nameOrHtml: string, options: { props?: CalendarProps, slots?: Partial<CalendarSlots> }) => {

Check failure on line 13 in test/components/Calendar.spec.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest, 20)

Argument of type '(nameOrHtml: string, options: { props?: CalendarProps; slots?: Partial<CalendarSlots>; }) => Promise<void>' is not assignable to parameter of type '(...args: [string, { props: { as: string; }; }] | [string, { props: { class: string; }; }] | [string, { props: { ui: {}; }; }] | [string, { slots: { default: () => "Default slot"; }; }]) => Awaitable<void>'.
const html = await ComponentRender(nameOrHtml, options, Calendar)
expect(html).toMatchSnapshot()
})
})
Loading