121 lines
3.7 KiB
Svelte
121 lines
3.7 KiB
Svelte
<script lang="ts">
|
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
|
import * as Calendar from './index.js';
|
|
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
|
import type { ButtonVariant } from '../button/button.svelte';
|
|
import { isEqualMonth, type DateValue } from '@internationalized/date';
|
|
import type { Snippet } from 'svelte';
|
|
|
|
let {
|
|
ref = $bindable(null),
|
|
value = $bindable(),
|
|
placeholder = $bindable(),
|
|
class: className,
|
|
weekdayFormat = 'short',
|
|
buttonVariant = 'ghost',
|
|
captionLayout = 'label',
|
|
locale = 'en-US',
|
|
months: monthsProp,
|
|
years,
|
|
monthFormat: monthFormatProp,
|
|
yearFormat = 'numeric',
|
|
day,
|
|
disableDaysOutsideMonth = false,
|
|
...restProps
|
|
}: WithoutChildrenOrChild<CalendarPrimitive.RootProps> & {
|
|
buttonVariant?: ButtonVariant;
|
|
captionLayout?: 'dropdown' | 'dropdown-months' | 'dropdown-years' | 'label';
|
|
months?: CalendarPrimitive.MonthSelectProps['months'];
|
|
years?: CalendarPrimitive.YearSelectProps['years'];
|
|
monthFormat?: CalendarPrimitive.MonthSelectProps['monthFormat'];
|
|
yearFormat?: CalendarPrimitive.YearSelectProps['yearFormat'];
|
|
day?: Snippet<[{ day: DateValue; outsideMonth: boolean }]>;
|
|
} = $props();
|
|
|
|
const monthFormat = $derived.by(() => {
|
|
if (monthFormatProp) return monthFormatProp;
|
|
if (captionLayout.startsWith('dropdown')) return 'short';
|
|
return 'long';
|
|
});
|
|
</script>
|
|
|
|
<!--
|
|
Discriminated Unions + Destructing (required for bindable) do not
|
|
get along, so we shut typescript up by casting `value` to `never`.
|
|
-->
|
|
<CalendarPrimitive.Root
|
|
bind:value={value as never}
|
|
bind:ref
|
|
bind:placeholder
|
|
{weekdayFormat}
|
|
{disableDaysOutsideMonth}
|
|
class={cn(
|
|
'group/calendar bg-background p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent',
|
|
// Add fuchsia theme styles for selected date
|
|
'[&_[data-selected]]:border-primary [&_[data-selected]]:text-primary [&_[data-selected]]:rounded-md [&_[data-selected]]:bg-fuchsia-200 [&_[data-selected]]:shadow-sm',
|
|
'[&_[data-selected]:hover]:bg-fuchsia-50',
|
|
// Ensure today indicator works with fuchsia theme
|
|
'[&_[data-today]:not([data-selected])]:bg-accent [&_[data-today]:not([data-selected])]:text-accent-foreground',
|
|
className
|
|
)}
|
|
{locale}
|
|
{monthFormat}
|
|
{yearFormat}
|
|
{...restProps}
|
|
>
|
|
{#snippet children({ months, weekdays })}
|
|
<Calendar.Months>
|
|
<Calendar.Nav>
|
|
<Calendar.PrevButton variant={buttonVariant} />
|
|
<Calendar.NextButton variant={buttonVariant} />
|
|
</Calendar.Nav>
|
|
{#each months as month, monthIndex (month)}
|
|
<Calendar.Month>
|
|
<Calendar.Header>
|
|
<Calendar.Caption
|
|
{captionLayout}
|
|
months={monthsProp}
|
|
{monthFormat}
|
|
{years}
|
|
{yearFormat}
|
|
month={month.value}
|
|
bind:placeholder
|
|
{locale}
|
|
{monthIndex}
|
|
/>
|
|
</Calendar.Header>
|
|
<Calendar.Grid>
|
|
<Calendar.GridHead>
|
|
<Calendar.GridRow class="select-none">
|
|
{#each weekdays as weekday (weekday)}
|
|
<Calendar.HeadCell>
|
|
{weekday.slice(0, 2)}
|
|
</Calendar.HeadCell>
|
|
{/each}
|
|
</Calendar.GridRow>
|
|
</Calendar.GridHead>
|
|
<Calendar.GridBody>
|
|
{#each month.weeks as weekDates (weekDates)}
|
|
<Calendar.GridRow class="mt-2 w-full">
|
|
{#each weekDates as date (date)}
|
|
<Calendar.Cell {date} month={month.value}>
|
|
{#if day}
|
|
{@render day({
|
|
day: date,
|
|
outsideMonth: !isEqualMonth(date, month.value)
|
|
})}
|
|
{:else}
|
|
<Calendar.Day />
|
|
{/if}
|
|
</Calendar.Cell>
|
|
{/each}
|
|
</Calendar.GridRow>
|
|
{/each}
|
|
</Calendar.GridBody>
|
|
</Calendar.Grid>
|
|
</Calendar.Month>
|
|
{/each}
|
|
</Calendar.Months>
|
|
{/snippet}
|
|
</CalendarPrimitive.Root>
|