feat: replace custom date picker with shadcn/ui Calendar + Popover (#98)

Replace the native HTML5 date input (hidden <input type="date"> with
showPicker()) with a proper shadcn/ui date picker using Calendar and
Popover components. The new implementation provides:

- Calendar popup with month navigation and keyboard support
- Clear date button in the popover footer
- Calendar icon in the trigger button
- Proper date parsing via parseDateValue helper

Also adds the design file design/date-picker-shadcn.pen with closed
and open state frames.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca Rossi
2026-02-26 19:17:40 +01:00
committed by GitHub
parent f351ed9a1d
commit 97d23cbf52
7 changed files with 695 additions and 32 deletions

View File

@@ -0,0 +1,285 @@
{
"children": [
{
"type": "frame",
"id": "datePickerClosed",
"x": 0,
"y": 0,
"name": "Date Picker — closed state (showing formatted date as button)",
"width": 320,
"height": 40,
"fill": "#ffffff",
"gap": 8,
"padding": 6,
"alignItems": "center",
"children": [
{
"type": "text",
"id": "dateClosedLabel",
"name": "label",
"fill": "#6b7280",
"content": "deadline",
"fontFamily": "IBM Plex Mono",
"fontSize": 10,
"fontWeight": "normal",
"letterSpacing": 1.2,
"textTransform": "uppercase"
},
{
"type": "frame",
"id": "dateClosedButton",
"name": "date-button",
"width": "fill_container",
"justifyContent": "flex-end",
"alignItems": "center",
"gap": 4,
"children": [
{
"type": "text",
"id": "dateClosedIcon",
"name": "calendar-icon",
"fill": "#9ca3af",
"content": "📅",
"fontSize": 12
},
{
"type": "text",
"id": "dateClosedValue",
"name": "date-value",
"fill": "#374151",
"content": "Mar 31, 2026",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
}
]
}
]
},
{
"type": "frame",
"id": "datePickerOpen",
"x": 0,
"y": 160,
"name": "Date Picker — open state (calendar popup below button)",
"width": 320,
"height": 380,
"fill": "#ffffff",
"layout": "vertical",
"gap": 4,
"padding": 0,
"children": [
{
"type": "frame",
"id": "dateOpenRow",
"name": "property-row",
"width": "fill_container",
"height": 40,
"gap": 8,
"padding": 6,
"alignItems": "center",
"children": [
{
"type": "text",
"id": "dateOpenLabel",
"name": "label",
"fill": "#6b7280",
"content": "deadline",
"fontFamily": "IBM Plex Mono",
"fontSize": 10,
"fontWeight": "normal",
"letterSpacing": 1.2,
"textTransform": "uppercase"
},
{
"type": "frame",
"id": "dateOpenButton",
"name": "date-button-active",
"width": "fill_container",
"justifyContent": "flex-end",
"alignItems": "center",
"gap": 4,
"children": [
{
"type": "text",
"id": "dateOpenIcon",
"name": "calendar-icon",
"fill": "#6366f1",
"content": "📅",
"fontSize": 12
},
{
"type": "text",
"id": "dateOpenValue",
"name": "date-value",
"fill": "#111827",
"content": "Mar 31, 2026",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "600"
}
]
}
]
},
{
"type": "frame",
"id": "calendarPopup",
"name": "calendar-popup",
"width": 280,
"height": 320,
"fill": "#ffffff",
"layout": "vertical",
"cornerRadius": 8,
"padding": 12,
"gap": 8,
"stroke": {
"align": "outside",
"thickness": 1,
"fill": "#e5e7eb"
},
"children": [
{
"type": "frame",
"id": "calendarHeader",
"name": "calendar-header",
"width": "fill_container",
"height": 32,
"alignItems": "center",
"justifyContent": "space-between",
"children": [
{
"type": "text",
"id": "calPrevBtn",
"name": "prev-month",
"fill": "#6b7280",
"content": "◀",
"fontSize": 12
},
{
"type": "text",
"id": "calMonthYear",
"name": "month-year",
"fill": "#111827",
"content": "March 2026",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "600"
},
{
"type": "text",
"id": "calNextBtn",
"name": "next-month",
"fill": "#6b7280",
"content": "▶",
"fontSize": 12
}
]
},
{
"type": "frame",
"id": "calWeekdayRow",
"name": "weekday-headers",
"width": "fill_container",
"height": 24,
"alignItems": "center",
"justifyContent": "space-between",
"children": [
{ "type": "text", "id": "wd1", "fill": "#9ca3af", "content": "Su", "fontSize": 11, "fontFamily": "Inter", "fontWeight": "500" },
{ "type": "text", "id": "wd2", "fill": "#9ca3af", "content": "Mo", "fontSize": 11, "fontFamily": "Inter", "fontWeight": "500" },
{ "type": "text", "id": "wd3", "fill": "#9ca3af", "content": "Tu", "fontSize": 11, "fontFamily": "Inter", "fontWeight": "500" },
{ "type": "text", "id": "wd4", "fill": "#9ca3af", "content": "We", "fontSize": 11, "fontFamily": "Inter", "fontWeight": "500" },
{ "type": "text", "id": "wd5", "fill": "#9ca3af", "content": "Th", "fontSize": 11, "fontFamily": "Inter", "fontWeight": "500" },
{ "type": "text", "id": "wd6", "fill": "#9ca3af", "content": "Fr", "fontSize": 11, "fontFamily": "Inter", "fontWeight": "500" },
{ "type": "text", "id": "wd7", "fill": "#9ca3af", "content": "Sa", "fontSize": 11, "fontFamily": "Inter", "fontWeight": "500" }
]
},
{
"type": "frame",
"id": "calGrid",
"name": "calendar-grid",
"width": "fill_container",
"height": "fill_container",
"layout": "vertical",
"gap": 2,
"children": [
{
"type": "frame",
"id": "calRow1",
"name": "week-1",
"width": "fill_container",
"height": 32,
"alignItems": "center",
"justifyContent": "space-between",
"children": [
{ "type": "text", "id": "d1", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" },
{ "type": "text", "id": "d2", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" },
{ "type": "text", "id": "d3", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" },
{ "type": "text", "id": "d4", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" },
{ "type": "text", "id": "d5", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" },
{ "type": "text", "id": "d6", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" },
{ "type": "text", "id": "d7", "fill": "#374151", "content": "1", "fontSize": 12, "fontFamily": "Inter" }
]
},
{
"type": "frame",
"id": "calRow5",
"name": "week-5-selected",
"width": "fill_container",
"height": 32,
"alignItems": "center",
"justifyContent": "space-between",
"children": [
{ "type": "text", "id": "d29", "fill": "#374151", "content": "29", "fontSize": 12, "fontFamily": "Inter" },
{ "type": "text", "id": "d30", "fill": "#374151", "content": "30", "fontSize": 12, "fontFamily": "Inter" },
{
"type": "frame",
"id": "selectedDay",
"name": "selected-day-31",
"width": 28,
"height": 28,
"fill": "#111827",
"cornerRadius": 14,
"alignItems": "center",
"justifyContent": "center",
"children": [
{ "type": "text", "id": "d31", "fill": "#ffffff", "content": "31", "fontSize": 12, "fontFamily": "Inter", "fontWeight": "600" }
]
},
{ "type": "text", "id": "d32", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" },
{ "type": "text", "id": "d33", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" },
{ "type": "text", "id": "d34", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" },
{ "type": "text", "id": "d35", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" }
]
}
]
},
{
"type": "frame",
"id": "calFooter",
"name": "calendar-footer",
"width": "fill_container",
"height": 28,
"alignItems": "center",
"justifyContent": "flex-end",
"gap": 8,
"children": [
{
"type": "text",
"id": "calClearBtn",
"name": "clear-button",
"fill": "#6b7280",
"content": "Clear",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "500"
}
]
}
]
}
]
}
],
"variables": {}
}

View File

@@ -40,11 +40,13 @@
"@tauri-apps/plugin-updater": "^2.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"katex": "^0.16.28",
"lowlight": "^3.3.0",
"lucide-react": "^0.564.0",
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-day-picker": "^9.13.2",
"react-dom": "^19.2.0",
"react-virtuoso": "^4.18.1",
"tailwind-merge": "^3.4.1",

34
pnpm-lock.yaml generated
View File

@@ -80,6 +80,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
date-fns:
specifier: ^4.1.0
version: 4.1.0
katex:
specifier: ^0.16.28
version: 0.16.28
@@ -95,6 +98,9 @@ importers:
react:
specifier: ^19.2.0
version: 19.2.4
react-day-picker:
specifier: ^9.13.2
version: 9.13.2(react@19.2.4)
react-dom:
specifier: ^19.2.0
version: 19.2.4(react@19.2.4)
@@ -346,6 +352,9 @@ packages:
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
engines: {node: '>=20.19.0'}
'@date-fns/tz@1.4.1':
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
'@dnd-kit/accessibility@3.1.1':
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
peerDependencies:
@@ -2172,6 +2181,12 @@ packages:
resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
date-fns-jalali@4.1.0-0:
resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -3071,6 +3086,12 @@ packages:
'@types/react-dom':
optional: true
react-day-picker@9.13.2:
resolution: {integrity: sha512-IMPiXfXVIAuR5Yk58DDPBC8QKClrhdXV+Tr/alBrwrHUw0qDDYB1m5zPNuTnnPIr/gmJ4ChMxmtqPdxm8+R4Eg==}
engines: {node: '>=18'}
peerDependencies:
react: '>=16.8.0'
react-dom@19.2.4:
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
peerDependencies:
@@ -3874,6 +3895,8 @@ snapshots:
'@csstools/css-tokenizer@4.0.0': {}
'@date-fns/tz@1.4.1': {}
'@dnd-kit/accessibility@3.1.1(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -5630,6 +5653,10 @@ snapshots:
transitivePeerDependencies:
- '@noble/hashes'
date-fns-jalali@4.1.0-0: {}
date-fns@4.1.0: {}
debug@4.4.3:
dependencies:
ms: 2.1.3
@@ -6817,6 +6844,13 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
react-day-picker@9.13.2(react@19.2.4):
dependencies:
'@date-fns/tz': 1.4.1
date-fns: 4.1.0
date-fns-jalali: 4.1.0-0
react: 19.2.4
react-dom@19.2.4(react@19.2.4):
dependencies:
react: 19.2.4

View File

@@ -24,8 +24,8 @@ beforeAll(() => {
Element.prototype.hasPointerCapture = () => false
Element.prototype.setPointerCapture = vi.fn()
Element.prototype.releasePointerCapture = vi.fn()
// showPicker is not available in JSDOM
HTMLInputElement.prototype.showPicker = vi.fn()
// Radix Popover needs getComputedStyle in JSDOM
if (!window.getComputedStyle) window.getComputedStyle = vi.fn().mockReturnValue({}) as never
})
const makeEntry = (overrides: Partial<VaultEntry> = {}): VaultEntry => ({
@@ -691,7 +691,7 @@ describe('DynamicPropertiesPanel', () => {
expect(screen.getByText('Mar 31, 2026')).toBeInTheDocument()
})
it('renders hidden date picker input', () => {
it('renders calendar icon in date trigger button', () => {
render(
<DynamicPropertiesPanel
entry={makeEntry()}
@@ -700,10 +700,12 @@ describe('DynamicPropertiesPanel', () => {
onUpdateProperty={onUpdateProperty}
/>
)
expect(screen.getByTestId('date-picker-input')).toBeInTheDocument()
const trigger = screen.getByTestId('date-display')
expect(trigger.tagName).toBe('BUTTON')
expect(trigger.querySelector('svg')).toBeInTheDocument()
})
it('calls onUpdateProperty when date picker value changes', () => {
it('opens calendar popover when date button clicked', () => {
render(
<DynamicPropertiesPanel
entry={makeEntry()}
@@ -712,9 +714,10 @@ describe('DynamicPropertiesPanel', () => {
onUpdateProperty={onUpdateProperty}
/>
)
const dateInput = screen.getByTestId('date-picker-input')
fireEvent.change(dateInput, { target: { value: '2026-04-15' } })
expect(onUpdateProperty).toHaveBeenCalledWith('deadline', '2026-04-15')
fireEvent.click(screen.getByTestId('date-display'))
expect(screen.getByTestId('date-picker-popover')).toBeInTheDocument()
// Clear button is inside the popover portal
expect(screen.getByText('Clear date')).toBeInTheDocument()
})
})

View File

@@ -5,7 +5,10 @@ import type { ParsedFrontmatter } from '../utils/frontmatter'
import { EditableValue, TagPillList, UrlValue } from './EditableValue'
import { isUrlValue } from '../utils/url'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from '@/components/ui/select'
import { CalendarIcon, XIcon } from 'lucide-react'
import { getTypeColor, getTypeLightColor } from '../utils/typeColors'
import { countWords } from '../utils/wikilinks'
import { Check, X, Type, Calendar, ToggleLeft, Circle, Link } from 'lucide-react'
@@ -103,40 +106,69 @@ function BooleanToggle({ value, onToggle }: { value: boolean; onToggle: () => vo
)
}
function parseDateValue(value: string): Date | undefined {
const iso = toISODate(value)
const d = new Date(iso + 'T00:00:00')
return isNaN(d.getTime()) ? undefined : d
}
function DateValue({ value, onSave }: {
value: string; onSave: (newValue: string) => void
}) {
const inputRef = useRef<HTMLInputElement>(null)
const [open, setOpen] = useState(false)
const formatted = formatDateValue(value)
const isoValue = toISODate(value)
const selectedDate = parseDateValue(value)
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value) onSave(e.target.value)
const handleSelect = (day: Date | undefined) => {
if (day) {
const yyyy = day.getFullYear()
const mm = String(day.getMonth() + 1).padStart(2, '0')
const dd = String(day.getDate()).padStart(2, '0')
onSave(`${yyyy}-${mm}-${dd}`)
}
setOpen(false)
}
const handleClick = () => {
inputRef.current?.showPicker()
const handleClear = (e: React.MouseEvent) => {
e.stopPropagation()
onSave('')
setOpen(false)
}
return (
<span className="relative inline-flex min-w-0 items-center" data-testid="date-display">
<span
className="min-w-0 cursor-pointer truncate rounded px-1 py-0.5 text-right text-[12px] text-secondary-foreground transition-colors hover:bg-muted"
onClick={handleClick}
title={value}
>
{formatted}
</span>
<input
ref={inputRef}
type="date"
className="pointer-events-none absolute inset-0 h-full w-full opacity-0"
value={isoValue}
onChange={handleDateChange}
tabIndex={-1}
data-testid="date-picker-input"
/>
</span>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
className="inline-flex min-w-0 cursor-pointer items-center gap-1 rounded border-none bg-transparent px-1 py-0.5 text-right text-[12px] text-secondary-foreground transition-colors hover:bg-muted"
title={value}
data-testid="date-display"
>
<CalendarIcon className="size-3 shrink-0 text-muted-foreground" />
<span className="min-w-0 truncate">{formatted}</span>
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end" data-testid="date-picker-popover">
<Calendar
mode="single"
selected={selectedDate}
onSelect={handleSelect}
defaultMonth={selectedDate}
data-testid="date-picker-calendar"
/>
{selectedDate && (
<div className="border-t px-3 py-2">
<button
className="inline-flex items-center gap-1 border-none bg-transparent text-xs text-muted-foreground transition-colors hover:text-foreground"
onClick={handleClear}
data-testid="date-picker-clear"
>
<XIcon className="size-3" />
Clear date
</button>
</div>
)}
</PopoverContent>
</Popover>
)
}

View File

@@ -0,0 +1,220 @@
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import {
DayPicker,
getDefaultClassNames,
type DayButton,
} from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
defaultClassNames.day
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }

View File

@@ -0,0 +1,87 @@
import * as React from "react"
import { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="popover-header"
className={cn("flex flex-col gap-1 text-sm", className)}
{...props}
/>
)
}
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
return (
<div
data-slot="popover-title"
className={cn("font-medium", className)}
{...props}
/>
)
}
function PopoverDescription({
className,
...props
}: React.ComponentProps<"p">) {
return (
<p
data-slot="popover-description"
className={cn("text-muted-foreground", className)}
{...props}
/>
)
}
export {
Popover,
PopoverTrigger,
PopoverContent,
PopoverAnchor,
PopoverHeader,
PopoverTitle,
PopoverDescription,
}